Permalink
Browse files

Initial commit

  • Loading branch information...
0 parents commit 15bed224aed5abb5eb57b51fe03b4ca071e72f0b @rauchg rauchg committed Sep 22, 2011
1 .gitignore
@@ -0,0 +1 @@
+node_modules
3 .npmignore
@@ -0,0 +1,3 @@
+test/
+support/
+README.md
57 README.md
@@ -0,0 +1,57 @@
+
+# Shorty
+
+Shorty is LearnBoost's URL shortening/redirection service.
+
+## Features
+
+- Redis backed
+- Super fast
+- At production use on https://lrn.bt
+- Uses express, jade, stylus. Easy to hack on!
+- Realtime stats with [socket.io](http://socket.io)
+ - **/**
+ ![](http://f.cl.ly/items/2h2k1p1b2E1I2y0N0Y3u/Image%202011.09.21%208:49:42%20PM.png)
+ - **/stats**
+ ![](http://f.cl.ly/items/072u3V453Q2X0p44180J/Image%202011.09.21%208:16:26%20PM.png)
+
+## API
+
+Post to `/create` with the `url` field to create.
+
+- If a field is missing or incorrect, status `400` is returned with a JSON
+body (`error` key)
+- If a problem saving the URL occurs, a status `500` is returned.
+- If the url is created, status `200` is returned with JSON body (`short` key)
+- If the url already exists, same response as creation is returned.
+
+## Credits
+
+(The MIT License)
+
+Copyright (c) 2011 Guillermo Rauch <guillermo@learnboost.com>
+
+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:
+
+The above copyright notice and this permission notice shall be
+included in all copies or substantial portions of the Software.
+
+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.
+
+### 3rd-party
+
+- Base60k library by Tantek Çelik
+- Icon by David Renelt for non-commercial use
+(http://www.iconarchive.com/show/little-icon-people-icons-by-david-renelt.html)
290 app.js
@@ -0,0 +1,290 @@
+
+/**
+ * Module dependencies.
+ */
+
+var express = require('express')
+ , stylus = require('stylus')
+ , sio = require('socket.io')
+ , fs = require('fs')
+ , base60 = require('./base60')
+ , url = require('url')
+ , jadevu = require('jadevu')
+
+/**
+ * Determine environment.
+ */
+
+var env = process.env.NODE_ENV || 'development';
+
+/**
+ * Create db.
+ */
+
+redis = require('redis').createClient();
@3rd-Eden
3rd-Eden Oct 22, 2011

var redis = :?

+
+/**
+ * App creator.
+ */
+
+module.exports = (function (port, secure) {
+
+ /**
+ * Create app.
+ */
+
+ var app;
+
+ if (secure) {
+ app = express.createServer({
+ key: read('ssl/' + env + '/key.key')
+ , cert: read('ssl/' + env + '/cert.crt')
+ });
+ } else {
+ app = express.createServer();
+ }
+
+ /**
+ * Basic middleware.
+ */
+
+ app.use(express.bodyParser());
+ app.use(stylus.middleware({ src: __dirname + '/public/', compile: css }));
+ app.use(express.static('public'));
+
+ /**
+ * Socket.IO
+ */
+
+ var io = sio.listen(app);
+
+ /**
+ * Reads a file
+ *
+ * @api private
+ */
+
+ function read (file) {
+ return fs.readFileSync(__dirname + '/' + file, 'utf8');
+ }
+
+ /**
+ * Stylus compiler
+ */
+
+ function css (str, path) {
+ return stylus(str)
+ .set('filename', path)
+ .set('compress', true)
+ .use(nib())
+ .import('nib');
+ };
+
+ /**
+ * Configure app.
+ */
+
+ app.configure(function () {
+ app.set('views', __dirname);
+ app.set('view engine', 'jade');
+ });
+
+ /**
+ * Development configuration.
+ */
+
+ app.configure('development', function () {
+ app.use(express.errorHandler({ dumpExceptions: true, showStack: true }));
+ });
+
+ /**
+ * Production configuration.
+ */
+
+ app.configure('production', function () {
+ app.use(express.errorHandler());
+ });
+
+ /**
+ * Index.
+ */
+
+ app.get('/', function (req, res, next) {
+ redis.hlen('urls', function (err, length) {
+ if (err) return next(err);
+ res.render('index', { count: length });
+ });
+ });
+
+ /**
+ * Create endpoint.
+ */
+
+ var locked = false
+ , queue = []
+
+ app.post('/create', validate, exists, function (req, res, next) {
+ if (locked) {
+ queue.push(req);
+
+ // allow a maximum of 5 seconds for processing
+ req.timer = setTimeout(function () {
+ queue.splice(queue.indexOf(req), 1);
+ res.send(503);
+ });
+
+ return;
+ } else {
+ handle(req);
+ }
+ });
+
+ /**
+ * Checkes that the URL is valid
+ */
+
+ function validate (req, res, next) {
+ var parsed = req.body.parsed = url.parse(req.body.url);
+
+ if (!req.body.url || !parsed.protocol || !parsed.host) {
+ return res.send(400, { error: 'Bad `url` field' });
+ }
+
+ next();
+ };
+
+ /**
+ * Checks that the URL doesnt exist already
+ */
+
+ function exists (req, res, next) {
+ redis.hget('urls-hash', req.body.url, function (err, val) {
+ if (err) return next(err);
+ if (val) return res.send({ short: val });
+ next();
+ });
+ }
+
+ /**
+ * Handles URL creation
+ */
+
+ function handle (req) {
+ var url = req.body.url
+ , parsed = req.body.parsed
+
+ // locking to ensure atomicity and uniqueness
+ locked = true;
+
+ // get count of urls
+ redis.hlen('urls', function (err, length) {
+ if (err) return req.next(500);
+
+ var short = base60.toString(length ? length + 1 : 0);
+
+ redis.hset('urls', short, url, function (err) {
+ if (err) return req.next(err);
+ redis.hset('urls-hash', url, short, function (err) {
+ if (err) return req.next(err);
+
+ var obj = {
+ type: 'url created'
+ , url: url
+ , short: short
+ , date: new Date
+ };
+
+ redis.lpush('transactions', JSON.stringify(obj), function (err) {
+ if (err) return req.next(500);
+
+ obj.parsed = parsed;
+ io.of('/main').volatile.emit('total', length + 1);
+ io.of('/stats').volatile.emit('url created', short, parsed, Date.now());
+ req.res.send({ short: short });
+
+ // check if there's another link to process
+ var next = queue.shift();
+ if (next) {
+ clearTimeout(next.timer);
+ handle(req);
+ } else {
+ // unlock
+ locked = false;
+ }
+ });
+ });
+ });
+ });
+ }
+
+ /**
+ * Stats page.
+ */
+
+ app.get('/stats', function (req, res, next) {
+ redis.lrange('transactions', 0, 100, function (err, vals) {
+ if (err) return next(err);
+ res.render('stats', { transactions: vals ? vals.map(function (v) {
+ v = JSON.parse(v);
+ v.parsed = url.parse(v.url);
+ delete v.url;
+ return v;
+ }).reverse() : [] });
+ });
+ });
+
+ /**
+ * Redirection.
+ */
+
+ app.get('/:short', function (req, res, next) {
+ redis.hget('urls', req.params.short, function (err, val) {
+ if (err) return next(err);
+ if (!val) return next();
+
+ redis.lpush('transactions', JSON.stringify({
+ type: 'url visited'
+ , url: val
+ , short: req.params.short
+ , date: Date.now()
+ , ip: req.socket.remoteAddress
+ , headers: req.headers
+ }), function (err) {
+ if (err) console.error(err);
+ });
+
+ io.of('/stats').volatile.emit(
+ 'url visited'
+ , req.params.short
+ , url.parse(val)
+ , Date.now()
+ );
+
+ res.redirect(val);
+ });
+ });
+
+ /**
+ * Listen.
+ */
+
+ if (!module.parent) {
+ app.listen(port, function () {
+ var addr = app.address();
+ console.error(
+ ' app listening on ' + addr.address + ':' + addr.port
+ + (secure ? ' (secure) ' : '')
+ );
+ });
+
+ if (!process.listeners('uncaughtException')) {
+ process.on('uncaughtException', function (e) {
+ console.error(e && e.stack ? e.stack : e);
+ });
+ }
+ }
+
+ return arguments.callee;
+})
+('production' == env ? 80 : 3000)
+('production' == env ? 443 : 3001, true)
66 base60.js
@@ -0,0 +1,66 @@
+
+/* Based on Tantek Çelik's NewBase60.
+ * http://tantek.com/
+ * http://tantek.pbworks.com/NewBase60
+ *
+ * Released under CC BY-SA 3.0 http://creativecommons.org/licenses/by-sa/3.0/
+ */
+
+/**
+ * Module exports.
+ */
+
+exports.toString = toString;
+exports.toNumber = toNumber;
+
+/**
+ * Converts a number to base60 str
+ *
+ * @param {Number} number to convert
+ * @api public
+ */
+
+function toString (number) {
+ var str = ""
+ , chars = "0123456789ABCDEFGHJKLMNPQRSTUVWXYZ_abcdefghijkmnopqrstuvwxyz"
+
+ if (undefined === number || 0 === number) return 0;
+
+ while (number > 0) {
+ var d = number % 60;
+ str = chars[d] + str;
+ number = (number - d) / 60;
+ }
+
+ return str;
+}
+
+/**
+ * Converts a base60 string to number
+ *
+ * @api {String} str
+ * @api public
+ */
+
+function toNumber (str) {
+ var number = 0;
+
+ for (var i = 0, l = str.length; i < l; i++) {
+ var c = str[i].charCodeAt(0);
+
+ if (c >= 48 && c <= 57) c = c - 48;
+ else if (c >= 65 && c <= 72) c -= 55;
+ else if (c == 73 || c == 108) c = 1;
+ else if (c >=74 && c<=78) c -= 56;
+ else if (c == 79) c = 0;
+ else if (c >= 80 && c <= 90) c -= 57;
+ else if (c == 95) c = 34;
+ else if (c >= 97 && c <= 107) c -= 62;
+ else if (c >= 109 && c <= 122) c -= 63;
+ else c = 0;
+
+ number = 60 * number + c;
+ }
+
+ return number;
+}
11 index.jade
@@ -0,0 +1,11 @@
+script(src="/socket.io/socket.io.js")
+script
+ window.onload = function () {
+ io.connect('/main')
+ .on('total', function (total) {
+ document.getElementById('count').innerHTML = total;
+ })
+ }
+
+p Welcome to the LearnBoost redirection service!
+p <b id="count">#{count}</b> urls generated so far!
59 layout.jade
@@ -0,0 +1,59 @@
+doctype 5
+html
+ head
+ title Shorty
+ :stylus
+ body
+ padding 20px 100px
+ font 14px/1.4 'helvetica neue', helvetica, arial, sans-serif
+ h1
+ font-size 26px
+ span
+ background url(/images/man.png) no-repeat right
+ display inline-block
+ padding-right 30px
+ h2
+ color #444
+ .creation-stats, .visited-stats
+ width 460px
+ height 500px
+ float left
+ margin-right 20px
+ padding-right 20px
+ overflow auto
+ h3
+ color #3B88D8
+ margin-top 0
+ .creation-stats
+ border-right 1px solid #ccc
+ .stat
+ padding 5px 10px 7px
+ background #fafafa
+ overflow hidden
+ div
+ font-size 11px
+ display inline-block
+ a
+ color #444
+ text-decoration none
+ &:hover
+ text-decoration underline
+ .type
+ font-weight bold
+ text-transform uppercase
+ width 100px
+ .date
+ color #999
+ width 80px
+ .short
+ width 40px
+ .url
+ width 220px
+ overflow hidden
+ height 15px
+ line-height 15px
+ &:nth-child(odd)
+ background #eee
+ body
+ h1: span Shorty
+ #wrap!= body
12 package.json
@@ -0,0 +1,12 @@
+{
+ "name": "shorty"
+ , "version": "0.0.1"
+ , "dependencies": {
+ "express": "2.4.6"
+ , "socket.io": "0.8.4"
+ , "redis": "0.6.7"
+ , "jade": "0.15.4"
+ , "jadevu": "0.0.7"
+ , "stylus": "0.15.4"
+ }
+}
BIN public/images/.DS_Store
Binary file not shown.
BIN public/images/bg.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
BIN public/images/man.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
17 public/js/date.js
@@ -0,0 +1,17 @@
+// By John Resig - licensed under MIT
+function prettyDate (time) {
+ var date = new Date(time),
+ diff = (((new Date()).getTime() - date.getTime()) / 1000),
+ day_diff = Math.floor(diff / 86400);
+ if ( isNaN(day_diff) || day_diff < 0 || day_diff >= 31 )
+ return;
+ return day_diff == 0 && (
+ diff < 60 && "just now" ||
+ diff < 120 && "1 minute ago" ||
+ diff < 3600 && Math.floor( diff / 60 ) + " minutes ago" ||
+ diff < 7200 && "1 hour ago" ||
+ diff < 86400 && Math.floor( diff / 3600 ) + " hours ago") ||
+ day_diff == 1 && "Yesterday" ||
+ day_diff < 7 && day_diff + " days ago" ||
+ day_diff < 31 && Math.ceil( day_diff / 7 ) + " weeks ago";
+}
2 ssl/README.md
@@ -0,0 +1,2 @@
+Place your SSL certificates under `<environment>/cert.crt` and
+`<environment>/key.key`.
21 ssl/development/cert.crt
@@ -0,0 +1,21 @@
+-----BEGIN CERTIFICATE-----
+MIIDXTCCAkWgAwIBAgIJAMUSOvlaeyQHMA0GCSqGSIb3DQEBBQUAMEUxCzAJBgNV
+BAYTAkFVMRMwEQYDVQQIDApTb21lLVN0YXRlMSEwHwYDVQQKDBhJbnRlcm5ldCBX
+aWRnaXRzIFB0eSBMdGQwHhcNMTAxMTE2MDkzMjQ5WhcNMTMxMTE1MDkzMjQ5WjBF
+MQswCQYDVQQGEwJBVTETMBEGA1UECAwKU29tZS1TdGF0ZTEhMB8GA1UECgwYSW50
+ZXJuZXQgV2lkZ2l0cyBQdHkgTHRkMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIB
+CgKCAQEAz+LXZOjcQCJq3+ZKUFabj71oo/ex/XsBcFqtBThjjTw9CVEVwfPQQp4X
+wtPiB204vnYXwQ1/R2NdTQqCZu47l79LssL/u2a5Y9+0NEU3nQA5qdt+1FAE0c5o
+exPimXOrR3GWfKz7PmZ2O0117IeCUUXPG5U8umhDe/4mDF4ZNJiKc404WthquTqg
+S7rLQZHhZ6D0EnGnOkzlmxJMYPNHSOY1/6ivdNUUcC87awNEA3lgfhy25IyBK3QJ
+c+aYKNTbt70Lery3bu2wWLFGtmNiGlQTS4JsxImRsECTI727ObS7/FWAQsqW+COL
+0Sa5BuMFrFIpjPrEe0ih7vRRbdmXRwIDAQABo1AwTjAdBgNVHQ4EFgQUDnV4d6mD
+tOnluLoCjkUHTX/n4agwHwYDVR0jBBgwFoAUDnV4d6mDtOnluLoCjkUHTX/n4agw
+DAYDVR0TBAUwAwEB/zANBgkqhkiG9w0BAQUFAAOCAQEAFwV4MQfTo+qMv9JMiyno
+IEiqfOz4RgtmBqRnXUffcjS2dhc7/z+FPZnM79Kej8eLHoVfxCyWRHFlzm93vEdv
+wxOCrD13EDOi08OOZfxWyIlCa6Bg8cMAKqQzd2OvQOWqlRWBTThBJIhWflU33izX
+Qn5GdmYqhfpc+9ZHHGhvXNydtRQkdxVK2dZNzLBvBlLlRmtoClU7xm3A+/5dddeP
+AQHEPtyFlUw49VYtZ3ru6KqPms7MKvcRhYLsy9rwSfuuniMlx4d0bDR7TOkw0QQS
+A0N8MGQRQpzl4mw4jLzyM5d5QtuGBh2P6hPGa0YQxtI3RPT/p6ENzzBiAKXiSfzo
+xw==
+-----END CERTIFICATE-----
27 ssl/development/key.key
@@ -0,0 +1,27 @@
+-----BEGIN RSA PRIVATE KEY-----
+MIIEowIBAAKCAQEAz+LXZOjcQCJq3+ZKUFabj71oo/ex/XsBcFqtBThjjTw9CVEV
+wfPQQp4XwtPiB204vnYXwQ1/R2NdTQqCZu47l79LssL/u2a5Y9+0NEU3nQA5qdt+
+1FAE0c5oexPimXOrR3GWfKz7PmZ2O0117IeCUUXPG5U8umhDe/4mDF4ZNJiKc404
+WthquTqgS7rLQZHhZ6D0EnGnOkzlmxJMYPNHSOY1/6ivdNUUcC87awNEA3lgfhy2
+5IyBK3QJc+aYKNTbt70Lery3bu2wWLFGtmNiGlQTS4JsxImRsECTI727ObS7/FWA
+QsqW+COL0Sa5BuMFrFIpjPrEe0ih7vRRbdmXRwIDAQABAoIBAGe4+9VqZfJN+dsq
+8Osyuz01uQ8OmC0sAWTIqUlQgENIyf9rCJsUBlYmwR5BT6Z69XP6QhHdpSK+TiAR
+XUz0EqG9HYzcxHIBaACP7j6iRoQ8R4kbbiWKo0z3WqQGIOqFjvD/mKEuQdE5mEYw
+eOUCG6BnX1WY2Yr8WKd2AA/tp0/Y4d8z04u9eodMpSTbHTzYMJb5SbBN1vo6FY7q
+8zSuO0BMzXlAxUsCwHsk1GQHFr8Oh3zIR7bQGtMBouI+6Lhh7sjFYsfxJboqMTBV
+IKaA216M6ggHG7MU1/jeKcMGDmEfqQLQoyWp29rMK6TklUgipME2L3UD7vTyAVzz
+xbVOpZkCgYEA8CXW4sZBBrSSrLR5SB+Ubu9qNTggLowOsC/kVKB2WJ4+xooc5HQo
+mFhq1v/WxPQoWIxdYsfg2odlL+JclK5Qcy6vXmRSdAQ5lK9gBDKxZSYc3NwAw2HA
+zyHCTK+I0n8PBYQ+yGcrxu0WqTGnlLW+Otk4CejO34WlgHwbH9bbY5UCgYEA3ZvT
+C4+OoMHXlmICSt29zUrYiL33IWsR3/MaONxTEDuvgkOSXXQOl/8Ebd6Nu+3WbsSN
+bjiPC/JyL1YCVmijdvFpl4gjtgvfJifs4G+QHvO6YfsYoVANk4u6g6rUuBIOwNK4
+RwYxwDc0oysp+g7tPxoSgDHReEVKJNzGBe9NGGsCgYEA4O4QP4gCEA3B9BF2J5+s
+n9uPVxmiyvZUK6Iv8zP4pThTBBMIzNIf09G9AHPQ7djikU2nioY8jXKTzC3xGTHM
+GJZ5m6fLsu7iH+nDvSreDSeNkTBfZqGAvoGYQ8uGE+L+ZuRfCcXYsxIOT5s6o4c3
+Dle2rVFpsuKzCY00urW796ECgYBn3go75+xEwrYGQSer6WR1nTgCV29GVYXKPooy
+zmmMOT1Yw80NSkEw0pFD4cTyqVYREsTrPU0mn1sPfrOXxnGfZSVFpcR/Je9QVfQ7
+eW7GYxwfom335aqHVj10SxRqteP+UoWWnHujCPz94VRKZMakBddYCIGSan+G6YdS
+7sdmwwKBgBc2qj0wvGXDF2kCLwSGfWoMf8CS1+5fIiUIdT1e/+7MfDdbmLMIFVjF
+QKS3zVViXCbrG5SY6wS9hxoc57f6E2A8vcaX6zy2xkZlGHQCpWRtEM5R01OWJQaH
+HsHMmQZGUQVoDm1oRkDhrTFK4K3ukc3rAxzeTZ96utOQN8/KJsTv
+-----END RSA PRIVATE KEY-----
57 stats.jade
@@ -0,0 +1,57 @@
+
+script(src="http://ajax.googleapis.com/ajax/libs/jquery/1.6.4/jquery.min.js")
+script(src="/socket.io/socket.io.js")
+script(src="/js/date.js")
+script
+ window.onload = function () {
+ function created (short, long, d, manual) {
+ var p = $('.creation-stats .stats');
+ var t = template('url created', { short: short, url: long, date: d }).prependTo(p);
+ }
+ function visited (short, long, d, manual) {
+ var p = $('.visited-stats .stats');
+ var t = template('url visited', { short: short, url: long, date: d }).prependTo(p);
+ }
+
+ io.connect('/stats')
+ .on('url created', created)
+ .on('url visited', visited)
+
+ for (var i = 0, l = transactions.length; i < l; i++) {
+ var t = transactions[i];
+ if ('url created' == t.type) {
+ created(t.short, t.parsed, t.date);
+ } else {
+ visited(t.short, t.parsed, t.date);
+ }
+ }
+ }
+script
+
+h2 Real-time stats
+
+.creation-stats
+ h3 Creations
+ .stats
+
+.visited-stats
+ h3 Visits
+ .stats
+
+template(id="url created"):
+ .stat.creation
+ .type URL Created
+ .date= prettyDate(date)
+ .short <a href="/#{short}">/#{short}</a>
+ .url <a href="#{url.href}"><em>#{url.hostname}</em><b>#{url.pathname}</b></a>
+
+template(id="url visited"):
+ .stat.visited
+ .type URL visited
+ .date= prettyDate(date)
+ .short <a href="/#{short}">/#{short}</a>
+ .url <a href="#{url.href}"><em>#{url.hostname}</em><b>#{url.pathname}</b></a>
+
+- var transactions = JSON.stringify(transactions)
+script
+ var transactions = !{transactions}

0 comments on commit 15bed22

Please sign in to comment.