├── .gitignore ├── CHECKS ├── views ├── error.hjs └── index.hjs ├── public ├── .well-known │ └── acme-challenge │ │ ├── DKSdEHdTKOw70LzbCQjfx3KmyC-BzNGChPr9iBXJ-RM │ │ └── qOcFW5W_SzcrfRG7lfHw0F9rhz7iWwbIjM9Lovonf1s └── stylesheets │ └── style.css ├── package.json ├── app.js ├── bin └── www └── routes └── api.js /.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules 2 | -------------------------------------------------------------------------------- /CHECKS: -------------------------------------------------------------------------------- 1 | /api/check Here I am. 2 | -------------------------------------------------------------------------------- /views/error.hjs: -------------------------------------------------------------------------------- 1 |

{{ message }}

2 |

{{ error.status }}

3 |
{{ error.stack }}
4 | -------------------------------------------------------------------------------- /public/.well-known/acme-challenge/DKSdEHdTKOw70LzbCQjfx3KmyC-BzNGChPr9iBXJ-RM: -------------------------------------------------------------------------------- 1 | DKSdEHdTKOw70LzbCQjfx3KmyC-BzNGChPr9iBXJ-RM.ZY9Yy_E5f8OY1k5ZULQJ7psaXKOO596BzJV2jC6Up8Q 2 | -------------------------------------------------------------------------------- /public/.well-known/acme-challenge/qOcFW5W_SzcrfRG7lfHw0F9rhz7iWwbIjM9Lovonf1s: -------------------------------------------------------------------------------- 1 | qOcFW5W_SzcrfRG7lfHw0F9rhz7iWwbIjM9Lovonf1s.ZY9Yy_E5f8OY1k5ZULQJ7psaXKOO596BzJV2jC6Up8Q 2 | -------------------------------------------------------------------------------- /public/stylesheets/style.css: -------------------------------------------------------------------------------- 1 | body { 2 | padding: 50px; 3 | font: 14px "Lucida Grande", Helvetica, Arial, sans-serif; 4 | } 5 | 6 | a { 7 | color: #00B7FF; 8 | } 9 | -------------------------------------------------------------------------------- /views/index.hjs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | {{ title }} 5 | 6 | 7 | 8 |

{{ title }}

9 |

Welcome to {{ title }}

10 | 11 | 12 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "cran-browser.app", 3 | "version": "0.0.0", 4 | "private": true, 5 | "scripts": { 6 | "start": "node ./bin/www" 7 | }, 8 | "dependencies": { 9 | "body-parser": "~1.13.2", 10 | "cookie-parser": "~1.3.5", 11 | "debug": "~2.2.0", 12 | "express": "~4.13.1", 13 | "got": "^6.3.0", 14 | "hjs": "~0.0.6", 15 | "isarray": "^1.0.0", 16 | "morgan": "~1.6.1", 17 | "nano": "^6.2.0", 18 | "serve-favicon": "~2.3.0" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /app.js: -------------------------------------------------------------------------------- 1 | var express = require('express'); 2 | var path = require('path'); 3 | var favicon = require('serve-favicon'); 4 | var logger = require('morgan'); 5 | var cookieParser = require('cookie-parser'); 6 | var bodyParser = require('body-parser'); 7 | 8 | var api = require('./routes/api'); 9 | 10 | var app = express(); 11 | 12 | // view engine setup 13 | app.set('views', path.join(__dirname, 'views')); 14 | app.set('view engine', 'hjs'); 15 | 16 | // uncomment after placing your favicon in /public 17 | //app.use(favicon(path.join(__dirname, 'public', 'favicon.ico'))); 18 | app.use(logger('dev')); 19 | app.use(bodyParser.json({ limit: '5mb' })); 20 | app.use(bodyParser.urlencoded({ extended: false, limit: '5mb' })); 21 | app.use(cookieParser()); 22 | app.use(express.static(path.join(__dirname, 'public'))); 23 | 24 | app.use('/api/', api); 25 | 26 | // catch 404 and forward to error handler 27 | app.use(function(req, res, next) { 28 | var err = new Error('Not Found'); 29 | err.status = 404; 30 | next(err); 31 | }); 32 | 33 | // error handlers 34 | 35 | // development error handler 36 | // will print stacktrace 37 | if (app.get('env') === 'development') { 38 | app.use(function(err, req, res, next) { 39 | res.status(err.status || 500); 40 | res.render('error', { 41 | message: err.message, 42 | error: err 43 | }); 44 | }); 45 | } 46 | 47 | // production error handler 48 | // no stacktraces leaked to user 49 | app.use(function(err, req, res, next) { 50 | res.status(err.status || 500); 51 | res.render('error', { 52 | message: err.message, 53 | error: {} 54 | }); 55 | }); 56 | 57 | 58 | module.exports = app; 59 | -------------------------------------------------------------------------------- /bin/www: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | /** 4 | * Module dependencies. 5 | */ 6 | 7 | var app = require('../app'); 8 | var debug = require('debug')('cran-browser.app:server'); 9 | var http = require('http'); 10 | 11 | /** 12 | * Get port from environment and store in Express. 13 | */ 14 | 15 | var port = normalizePort(process.env.PORT || '3000'); 16 | app.set('port', port); 17 | 18 | /** 19 | * Create HTTP server. 20 | */ 21 | 22 | var server = http.createServer(app); 23 | 24 | /** 25 | * Listen on provided port, on all network interfaces. 26 | */ 27 | 28 | server.listen(port); 29 | server.on('error', onError); 30 | server.on('listening', onListening); 31 | 32 | /** 33 | * Normalize a port into a number, string, or false. 34 | */ 35 | 36 | function normalizePort(val) { 37 | var port = parseInt(val, 10); 38 | 39 | if (isNaN(port)) { 40 | // named pipe 41 | return val; 42 | } 43 | 44 | if (port >= 0) { 45 | // port number 46 | return port; 47 | } 48 | 49 | return false; 50 | } 51 | 52 | /** 53 | * Event listener for HTTP server "error" event. 54 | */ 55 | 56 | function onError(error) { 57 | if (error.syscall !== 'listen') { 58 | throw error; 59 | } 60 | 61 | var bind = typeof port === 'string' 62 | ? 'Pipe ' + port 63 | : 'Port ' + port; 64 | 65 | // handle specific listen errors with friendly messages 66 | switch (error.code) { 67 | case 'EACCES': 68 | console.error(bind + ' requires elevated privileges'); 69 | process.exit(1); 70 | break; 71 | case 'EADDRINUSE': 72 | console.error(bind + ' is already in use'); 73 | process.exit(1); 74 | break; 75 | default: 76 | throw error; 77 | } 78 | } 79 | 80 | /** 81 | * Event listener for HTTP server "listening" event. 82 | */ 83 | 84 | function onListening() { 85 | var addr = server.address(); 86 | var bind = typeof addr === 'string' 87 | ? 'pipe ' + addr 88 | : 'port ' + addr.port; 89 | debug('Listening on ' + bind); 90 | } 91 | -------------------------------------------------------------------------------- /routes/api.js: -------------------------------------------------------------------------------- 1 | 2 | var db_url = process.env.DOCSDB_URL; 3 | 4 | var express = require('express'); 5 | var router = express.Router(); 6 | var got = require('got'); 7 | var stream = require('stream'); 8 | var url = require('url'); 9 | var nano = require('nano')(db_url); 10 | var isarray = require('isarray'); 11 | 12 | var pkg_re = new RegExp("^/map/([\\w0-9\\.]+)$"); 13 | 14 | router.get("/check", function(req, res) { 15 | res.send("Here I am."); 16 | }); 17 | 18 | router.get(pkg_re, function(req, res) { 19 | var package = req.params[0]; 20 | var auth = pkg_url_auth(db_url); 21 | var url = pkg_url(db_url, package); 22 | res.set('Content-Type', 'application/json'); 23 | got.stream(url, { auth: auth }).pipe(res); 24 | }); 25 | 26 | router.put(pkg_re, function(req, res) { 27 | var package = req.params[0]; 28 | var db = nano.use('code'); 29 | var doc = req.body; 30 | res.set('Content-Type', 'application/json'); 31 | 32 | doc._id = package; 33 | 34 | db.get(package, function(err, body) { 35 | if (!err) { doc._rev = body._rev; } 36 | 37 | db.insert(doc, package, function(err, body) { 38 | if (err) { 39 | if (err) { return handle_error(err, res); } 40 | } else { 41 | res.send(JSON.stringify({ 'result': 'OK' })); 42 | } 43 | }); 44 | }); 45 | }); 46 | 47 | // Find the position of a single function 48 | router.get( 49 | new RegExp("^/findfunction/([\\w0-9\\.]+)/(.*)$"), 50 | function(req, res) { 51 | var package = req.params[0]; 52 | var func = req.params[1]; 53 | var db = nano.use('code'); 54 | 55 | res.set('Content-Type', 'application/json'); 56 | db.get(package, function(err, body) { 57 | if (err) { return handle_error(err, res); } 58 | var fun = body.functions 59 | .filter(function(x) { return x.ID == func; }); 60 | fun = fun[0]; 61 | if (fun) { 62 | res.send(fun); 63 | } else { 64 | res.set(404) 65 | .send('{ "result": "error", "error": "Not found" }'); 66 | } 67 | }); 68 | } 69 | ) 70 | 71 | // Redirect to a function 72 | router.get( 73 | new RegExp("^/redirect/([\\w0-9\\.]+)/(.*)$"), 74 | function(req, res) { 75 | var package = req.params[0]; 76 | var func = req.params[1]; 77 | var db = nano.use('code'); 78 | 79 | db.get(package, function(err, body) { 80 | var url; 81 | url = 'https://github.com/cran/' + package; 82 | if (! err) { 83 | var fun = body.functions 84 | .filter(function(x) { return x.ID == func; }); 85 | fun = fun[0]; 86 | if (fun && fun.file) { 87 | url = 'https://github.com/cran/' + package + 88 | '/blob/master/' + fun.file + '#L' + fun.line; 89 | } 90 | } 91 | 92 | res.redirect(301, url); 93 | }); 94 | } 95 | ) 96 | 97 | // Redirect to manual 98 | router.get( 99 | new RegExp("^/redirectdocs/([\\w0-9\\.]+)/(.*)$"), 100 | function(req, res) { 101 | var package = req.params[0]; 102 | var func = req.params[1]; 103 | var db = nano.use('docs'); 104 | 105 | db.get(package, function(err, body) { 106 | var url = 'http://search.r-project.org/library/' + 107 | package + '/html/00Index.html'; 108 | 109 | if (!err) { 110 | var pages = Object.keys(body); 111 | var hits = pages.filter(function(x) { 112 | return isarray(body[x]) && body[x].indexOf(func) > -1 113 | }); 114 | console.log(pages); 115 | console.log(hits); 116 | if (hits.length) { 117 | var html = hits[0].replace(/\.Rd$/, '.html'); 118 | url = url.replace(/00Index\.html$/, html); 119 | } 120 | } 121 | 122 | res.redirect(301, url); 123 | }); 124 | } 125 | ) 126 | 127 | // Get all links for a file 128 | 129 | router.get( 130 | new RegExp("^/filelinks/([\\w0-9\\.]+)/(.*)$"), 131 | function(req, res) { 132 | var package = req.params[0]; 133 | var file = req.params[1]; 134 | var db = nano.use('code'); 135 | 136 | res.set('Content-Type', 'application/json'); 137 | 138 | db.get(package, function(err, body) { 139 | if (err) { return handle_error(err, res); } 140 | var calls = body.calls 141 | .filter(function(x) { return x.file == file; }); 142 | res.send(calls); 143 | }); 144 | } 145 | ) 146 | 147 | function handle_error(err, res) { 148 | res.set('Content-Type', 'application/json') 149 | .set(500) 150 | .send(JSON.stringify({ 'result': 'error', 'error': err })); 151 | } 152 | 153 | // Get the auth part only 154 | function pkg_url_auth(db_url) { 155 | return url.parse(db_url).auth; 156 | } 157 | 158 | // Need to drop 'auth' from here 159 | function pkg_url(db_url, package) { 160 | var parsed = url.parse(db_url); 161 | parsed.auth = null; 162 | parsed.path = parsed.pathname = '/code/' + package; 163 | return url.format(parsed); 164 | } 165 | 166 | module.exports = router; 167 | --------------------------------------------------------------------------------