oso 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/.gems +4 -0
- data/.gemtest +0 -0
- data/CHANGELOG.rdoc +3 -0
- data/Manifest.txt +17 -0
- data/README.rdoc +94 -0
- data/Rakefile +16 -0
- data/config.ru +7 -0
- data/lib/oso/public/audiosocket.png +0 -0
- data/lib/oso/public/favicon.ico +0 -0
- data/lib/oso/public/index.css +55 -0
- data/lib/oso/public/index.html +45 -0
- data/lib/oso/public/robots.txt +2 -0
- data/lib/oso/public/style.css +94 -0
- data/lib/oso/server.rb +107 -0
- data/lib/oso/views/layout.rhtml +22 -0
- data/lib/oso/views/stats.rhtml +62 -0
- data/lib/oso.rb +85 -0
- data/test/test_oso.rb +54 -0
- metadata +123 -0
data/.gems
ADDED
data/.gemtest
ADDED
|
File without changes
|
data/CHANGELOG.rdoc
ADDED
data/Manifest.txt
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
.gems
|
|
2
|
+
CHANGELOG.rdoc
|
|
3
|
+
Manifest.txt
|
|
4
|
+
README.rdoc
|
|
5
|
+
Rakefile
|
|
6
|
+
config.ru
|
|
7
|
+
lib/oso.rb
|
|
8
|
+
lib/oso/public/audiosocket.png
|
|
9
|
+
lib/oso/public/favicon.ico
|
|
10
|
+
lib/oso/public/index.css
|
|
11
|
+
lib/oso/public/index.html
|
|
12
|
+
lib/oso/public/robots.txt
|
|
13
|
+
lib/oso/public/style.css
|
|
14
|
+
lib/oso/server.rb
|
|
15
|
+
lib/oso/views/layout.rhtml
|
|
16
|
+
lib/oso/views/stats.rhtml
|
|
17
|
+
test/test_oso.rb
|
data/README.rdoc
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
= Oso
|
|
2
|
+
|
|
3
|
+
* http://github.com/audiosocket/oso
|
|
4
|
+
|
|
5
|
+
== Description
|
|
6
|
+
|
|
7
|
+
Audiosocket's minimal URL shortener. It's named "oso" because those
|
|
8
|
+
three letters look a bit like our logo.
|
|
9
|
+
|
|
10
|
+
== Configuring
|
|
11
|
+
|
|
12
|
+
We deploy 1.9.2, so Oso expects it. It shouldn't be too hard to port
|
|
13
|
+
to 1.8 if you feel the need.
|
|
14
|
+
|
|
15
|
+
If you install the deps in the <tt>.gems</tt> file you can run
|
|
16
|
+
<tt>rackup</tt> by itself.
|
|
17
|
+
|
|
18
|
+
Oso needs Redis. Set the <tt>OSO_REDIS_URL</tt> environment variable,
|
|
19
|
+
which defaults to <tt>redis://localhost:6379</tt>. All keys are
|
|
20
|
+
namespaced under <tt>oso:</tt>.
|
|
21
|
+
|
|
22
|
+
== Using
|
|
23
|
+
|
|
24
|
+
There's a static HTML index that'll let you shorten URLs by hand, but
|
|
25
|
+
we mostly use Oso inside other apps. To shorten a URL, <tt>POST</tt>
|
|
26
|
+
to <tt>/</tt> with a <tt>url</tt> parameter. It'll return a
|
|
27
|
+
<tt>201</tt> with the shortened URL in the body.
|
|
28
|
+
|
|
29
|
+
You can optionally pass a <tt>life</tt> parameter, which is the
|
|
30
|
+
lifetime of the shortened URL in seconds.
|
|
31
|
+
|
|
32
|
+
You can optionally pass a <tt>limit</tt> parameter, which is the number
|
|
33
|
+
of times you want the shortened URL to work. After the limit is
|
|
34
|
+
reached <tt>404</tt>'s are returned.
|
|
35
|
+
|
|
36
|
+
You can optionally pass a <tt>name</tt> parameter, which will be used
|
|
37
|
+
instead of generating a token for the shortened URL. Names can only
|
|
38
|
+
contain alphanumerics and dashes.
|
|
39
|
+
|
|
40
|
+
=== From Ruby
|
|
41
|
+
|
|
42
|
+
If you install the <tt>oso</tt> RubyGem:
|
|
43
|
+
|
|
44
|
+
require "oso"
|
|
45
|
+
|
|
46
|
+
Oso.shorten "github.com" # => "http://oso.local/Acf"
|
|
47
|
+
Oso.shorten "github.com", life: 30 # => "http://oso.local/Ad0"
|
|
48
|
+
|
|
49
|
+
Set the <tt>OSO_URL</tt> environment variable to control where the
|
|
50
|
+
client looks for the server, or see the RDocs for information on
|
|
51
|
+
creating multiple instances.
|
|
52
|
+
|
|
53
|
+
== Deploying
|
|
54
|
+
|
|
55
|
+
If you fork it, Oso is already prepped for Heroku. It has an
|
|
56
|
+
up-to-date <tt>.gems</tt> manifest, and it'll use the
|
|
57
|
+
<tt>REDISTOGO_URL</tt> env var if it exists.
|
|
58
|
+
|
|
59
|
+
If you'd like to use it in your own <tt>config.ru</tt> file via the
|
|
60
|
+
gem, you'll need to provide the server's dependencies yourself. The
|
|
61
|
+
gem doesn't express any runtime dependencies since it's primarily used
|
|
62
|
+
as a client.
|
|
63
|
+
|
|
64
|
+
Assuming you've provided the server dependencies (see the
|
|
65
|
+
<tt>.gems</tt> file), using the gem from Rack is simple:
|
|
66
|
+
|
|
67
|
+
require "oso/server"
|
|
68
|
+
run Sinatra::Application
|
|
69
|
+
|
|
70
|
+
Pull requests making Oso a mountable <tt>Sinatra::Base</tt> app are
|
|
71
|
+
most welcome.
|
|
72
|
+
|
|
73
|
+
== License
|
|
74
|
+
|
|
75
|
+
Copyright 2011 Leopona Inc.
|
|
76
|
+
|
|
77
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
|
78
|
+
a copy of this software and associated documentation files (the
|
|
79
|
+
'Software'), to deal in the Software without restriction, including
|
|
80
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
|
81
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
|
82
|
+
permit persons to whom the Software is furnished to do so, subject to
|
|
83
|
+
the following conditions:
|
|
84
|
+
|
|
85
|
+
The above copyright notice and this permission notice shall be
|
|
86
|
+
included in all copies or substantial portions of the Software.
|
|
87
|
+
|
|
88
|
+
THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
|
|
89
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
|
90
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
|
91
|
+
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
|
92
|
+
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
|
|
93
|
+
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
|
94
|
+
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/Rakefile
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
require "hoe"
|
|
2
|
+
|
|
3
|
+
Hoe.plugin :doofus, :git
|
|
4
|
+
|
|
5
|
+
Hoe.spec "oso" do
|
|
6
|
+
developer "Audiosocket", "it@audiosocket.com"
|
|
7
|
+
|
|
8
|
+
self.extra_rdoc_files = Dir["*.rdoc"]
|
|
9
|
+
self.history_file = "CHANGELOG.rdoc"
|
|
10
|
+
self.readme_file = "README.rdoc"
|
|
11
|
+
self.testlib = :minitest
|
|
12
|
+
|
|
13
|
+
extra_dev_deps << ["fakeweb", "1.3.0"]
|
|
14
|
+
extra_dev_deps << ["minitest", "2.0.2"]
|
|
15
|
+
extra_dev_deps << ["mocha", "0.9.12"]
|
|
16
|
+
end
|
data/config.ru
ADDED
|
Binary file
|
|
File without changes
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
html {
|
|
2
|
+
background: #fff url(/audiosocket.png) no-repeat fixed center;
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
body {
|
|
6
|
+
font-family : "Helvetica Neue", sans-serif;
|
|
7
|
+
margin-top : 72px;
|
|
8
|
+
text-align : center;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
input {
|
|
12
|
+
background : #fff;
|
|
13
|
+
border : 1px solid #000;
|
|
14
|
+
color : #000;
|
|
15
|
+
font-size : 18px;
|
|
16
|
+
margin : 0;
|
|
17
|
+
padding : 0;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
#url {
|
|
21
|
+
border-bottom-left-radius : 4px;
|
|
22
|
+
border-top-left-radius : 4px;
|
|
23
|
+
padding : 6px;
|
|
24
|
+
width : 300px;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
#shorten {
|
|
28
|
+
-webkit-transition : 0.3s;
|
|
29
|
+
background : #000;
|
|
30
|
+
border-bottom-right-radius : 4px;
|
|
31
|
+
border-top-right-radius : 4px;
|
|
32
|
+
color : #777;
|
|
33
|
+
cursor : pointer;
|
|
34
|
+
padding : 6px;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
#shorten:hover {
|
|
38
|
+
color: #eee;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
#shortened input {
|
|
42
|
+
border : 0;
|
|
43
|
+
color : #333;
|
|
44
|
+
cursor : text !important;
|
|
45
|
+
font-size : 24px;
|
|
46
|
+
text-align : center;
|
|
47
|
+
width : 500px;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
#shortened p {
|
|
51
|
+
color : #666;
|
|
52
|
+
font-size : 13px;
|
|
53
|
+
margin-top : 6px;
|
|
54
|
+
}
|
|
55
|
+
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
|
|
3
|
+
<html>
|
|
4
|
+
<head>
|
|
5
|
+
<title>Oso</title>
|
|
6
|
+
<link href="/index.css" rel="stylesheet" type="text/css">
|
|
7
|
+
</head>
|
|
8
|
+
|
|
9
|
+
<body>
|
|
10
|
+
<form action="/" method="POST">
|
|
11
|
+
<input id="url" name="url" placeholder="http://example.org"
|
|
12
|
+
type="text"><input id="shorten" type="submit" value="Shorten">
|
|
13
|
+
</form>
|
|
14
|
+
|
|
15
|
+
<div id="shortened" style="display:none">
|
|
16
|
+
<input readonly>
|
|
17
|
+
<p>Copy and share this URL.</p>
|
|
18
|
+
</div>
|
|
19
|
+
|
|
20
|
+
<script type="text/javascript"
|
|
21
|
+
src="http://code.jquery.com/jquery-1.5.min.js"></script>
|
|
22
|
+
|
|
23
|
+
<script type="text/javascript">
|
|
24
|
+
$(function() {
|
|
25
|
+
$("form").submit(function() {
|
|
26
|
+
var url = $("#url").val()
|
|
27
|
+
|
|
28
|
+
if (!/^\s*$/.test(url)) {
|
|
29
|
+
$.post("/", { url: url }, function(short) {
|
|
30
|
+
$("form").hide()
|
|
31
|
+
$("#shortened").show()
|
|
32
|
+
$("#shortened input").val(short).select()
|
|
33
|
+
})
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
return false
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
$("#shortened").click(function() {
|
|
40
|
+
$("input", this).select()
|
|
41
|
+
})
|
|
42
|
+
})
|
|
43
|
+
</script>
|
|
44
|
+
</body>
|
|
45
|
+
</html>
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
* {
|
|
2
|
+
font-size : 16px;
|
|
3
|
+
margin : 0;
|
|
4
|
+
padding : 0;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
a {
|
|
8
|
+
color: #a00;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
a.btn {
|
|
12
|
+
-webkit-transition : .2s;
|
|
13
|
+
background : #ddd;
|
|
14
|
+
border-radius : 4px;
|
|
15
|
+
color : #333;
|
|
16
|
+
display : block;
|
|
17
|
+
font-size : 14px;
|
|
18
|
+
font-weight : normal;
|
|
19
|
+
padding : 4px 6px;
|
|
20
|
+
text-decoration : none;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
a.btn:hover {
|
|
24
|
+
background : #333;
|
|
25
|
+
color : #eee;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
a.btn:active {
|
|
29
|
+
color: #f00;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
body {
|
|
33
|
+
font-family : "Helvetica Neue", sans-serif;
|
|
34
|
+
width : 600px;
|
|
35
|
+
margin : 36px auto;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
h1 {
|
|
39
|
+
border-bottom : 1px solid #ccc;
|
|
40
|
+
font-size : 36px;
|
|
41
|
+
margin-bottom : 18px;
|
|
42
|
+
padding-bottom : 10px;
|
|
43
|
+
position : relative;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
h1 a {
|
|
47
|
+
bottom : 16px;
|
|
48
|
+
position : absolute;
|
|
49
|
+
right : 0;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
h2 {
|
|
53
|
+
clear : left;
|
|
54
|
+
font-size : 24px;
|
|
55
|
+
margin-bottom : 18px;
|
|
56
|
+
padding-top : 36px;
|
|
57
|
+
position : relative;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
table {
|
|
61
|
+
width: 100%;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
thead th {
|
|
65
|
+
border-bottom : 1px solid #ccc;
|
|
66
|
+
text-align : left;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
td, th {
|
|
70
|
+
padding: 3px;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
.boxen li {
|
|
74
|
+
background : #000;
|
|
75
|
+
border-radius : 8px;
|
|
76
|
+
color : #aaa;
|
|
77
|
+
float : left;
|
|
78
|
+
font-size : 13px;
|
|
79
|
+
list-style-type : none;
|
|
80
|
+
margin-left : 18px;
|
|
81
|
+
padding : 12px;
|
|
82
|
+
text-align : center;
|
|
83
|
+
width : 164px;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
.boxen li:first-child {
|
|
87
|
+
margin-left: 0;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
.boxen li .num {
|
|
91
|
+
color : #eee;
|
|
92
|
+
display : block;
|
|
93
|
+
font-size : 24px;
|
|
94
|
+
}
|
data/lib/oso/server.rb
ADDED
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
require "redis/namespace"
|
|
2
|
+
require "new_base_60"
|
|
3
|
+
require "sinatra"
|
|
4
|
+
require "uri"
|
|
5
|
+
|
|
6
|
+
url = ENV.values_at("OSO_REDIS_URL", "REDISTOGO_URL").compact.first
|
|
7
|
+
url = URI.parse url || "redis://localhost:6379"
|
|
8
|
+
|
|
9
|
+
$redis = Redis::Namespace.new :oso,
|
|
10
|
+
redis: Redis.new(host: url.host, password: url.password, port: url.port)
|
|
11
|
+
|
|
12
|
+
helpers do
|
|
13
|
+
def bad! message
|
|
14
|
+
halt 412, {}, message
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def nope!
|
|
18
|
+
halt 404, {}, "No luck."
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def save short, url, limit, life
|
|
22
|
+
longkey = "long:#{url}"
|
|
23
|
+
shortkey = "short:#{short}"
|
|
24
|
+
limitkey = "#{shortkey}:limit"
|
|
25
|
+
|
|
26
|
+
$redis.multi do
|
|
27
|
+
$redis.set limitkey, limit if limit
|
|
28
|
+
$redis.set longkey, short
|
|
29
|
+
$redis.set shortkey, url
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
if life
|
|
33
|
+
$redis.expire limitkey, limit if limit
|
|
34
|
+
$redis.expire longkey, life
|
|
35
|
+
$redis.expire shortkey, life
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def shorturl short
|
|
40
|
+
request.path_info = "/#{short}"
|
|
41
|
+
request.url
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
get "/" do
|
|
46
|
+
IO.read "#{settings.public}/index.html"
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
get "/stats" do
|
|
50
|
+
@count = $redis.get(:counter).to_i
|
|
51
|
+
@hits = $redis.get(:hits).to_i
|
|
52
|
+
@misses = $redis.get(:misses).to_i
|
|
53
|
+
@byhits = Hash[*$redis.zrevrange("by:hits", 0, 10, :with_scores => true)]
|
|
54
|
+
@bytimes = Hash[*$redis.zrevrange("by:time", 0, 10, :with_scores => true)]
|
|
55
|
+
|
|
56
|
+
[@byhits, @bytimes].each do |h|
|
|
57
|
+
h.each { |k, v| h[k] = { long: $redis.get("short:#{k}"), score: v } }
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
@title = "Stats"
|
|
61
|
+
erb :stats
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
post "/" do
|
|
65
|
+
bad! "Missing url." unless url = params[:url]
|
|
66
|
+
|
|
67
|
+
url = "http://#{url}" unless /^http/i =~ url
|
|
68
|
+
|
|
69
|
+
bad! "Malformed url." unless (u = URI.parse url) && /^http/ =~ u.scheme
|
|
70
|
+
|
|
71
|
+
life = params[:life].to_i if params[:life]
|
|
72
|
+
limit = params[:limit].to_i if params[:limit]
|
|
73
|
+
short = params[:name] if params[:name] && /[-a-z0-9]+/i =~ params[:name]
|
|
74
|
+
|
|
75
|
+
bad! "Name is already taken." if short && $redis.exists("short:#{short}")
|
|
76
|
+
|
|
77
|
+
if !short && existing = $redis.get("long:#{url}")
|
|
78
|
+
halt 201, {}, shorturl(existing)
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
short ||= $redis.incr(:counter).to_sxg
|
|
82
|
+
save short, url, limit, life
|
|
83
|
+
|
|
84
|
+
[201, {}, shorturl(short)]
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
get "/:short" do |short|
|
|
88
|
+
long = $redis.get "short:#{short}"
|
|
89
|
+
$redis.incr(:misses) and nope! unless long
|
|
90
|
+
|
|
91
|
+
limited = $redis.exists("short:#{short}:limit") &&
|
|
92
|
+
$redis.decr("short:#{short}:limit") < 0
|
|
93
|
+
|
|
94
|
+
if limited
|
|
95
|
+
%W(long:#{long} short:#{short} short:#{short}:limit).each do |key|
|
|
96
|
+
$redis.del key
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
nope!
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
$redis.incr :hits
|
|
103
|
+
$redis.zincrby "by:hits", 1, short
|
|
104
|
+
$redis.zadd "by:time", Time.now.utc.to_i, short
|
|
105
|
+
|
|
106
|
+
redirect long
|
|
107
|
+
end
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
|
|
3
|
+
<html>
|
|
4
|
+
<head>
|
|
5
|
+
<title>Oso<%= " :: #@title" if @title %></title>
|
|
6
|
+
<link href="/style.css" rel="stylesheet" type="text/css">
|
|
7
|
+
</head>
|
|
8
|
+
|
|
9
|
+
<body>
|
|
10
|
+
<% if @title %>
|
|
11
|
+
<h1>
|
|
12
|
+
Oso :: <%= @title %>
|
|
13
|
+
<a class="btn" href="/">home</a>
|
|
14
|
+
</h1>
|
|
15
|
+
<% end %>
|
|
16
|
+
|
|
17
|
+
<%= yield %>
|
|
18
|
+
|
|
19
|
+
<script type="text/javascript"
|
|
20
|
+
src="http://code.jquery.com/jquery-1.5.min.js"></script>
|
|
21
|
+
</body>
|
|
22
|
+
</html>
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
<ul class="boxen">
|
|
2
|
+
<li>
|
|
3
|
+
<span class="num"><%= @count %></span>
|
|
4
|
+
urls
|
|
5
|
+
</li>
|
|
6
|
+
|
|
7
|
+
<li>
|
|
8
|
+
<span class="num"><%= @hits %></span>
|
|
9
|
+
hits
|
|
10
|
+
</li>
|
|
11
|
+
|
|
12
|
+
<li>
|
|
13
|
+
<span class="num"><%= @misses %></span>
|
|
14
|
+
misses
|
|
15
|
+
</li>
|
|
16
|
+
</ul>
|
|
17
|
+
|
|
18
|
+
<% if @hits + @misses > 0 %>
|
|
19
|
+
<h2>Recent Hits</h2>
|
|
20
|
+
|
|
21
|
+
<table cellspacing="0">
|
|
22
|
+
<thead>
|
|
23
|
+
<tr>
|
|
24
|
+
<th>Last Hit</th>
|
|
25
|
+
<th>URL</th>
|
|
26
|
+
<th>Shortened</th>
|
|
27
|
+
</tr>
|
|
28
|
+
</thead>
|
|
29
|
+
|
|
30
|
+
<tbody>
|
|
31
|
+
<% @bytimes.each do |short, data| %>
|
|
32
|
+
<tr>
|
|
33
|
+
<td><%= data[:score] %></td>
|
|
34
|
+
<td><a href="<%= data[:long] %>"><%= data[:long] %></a></td>
|
|
35
|
+
<td><a href="<%= shorturl(short) %>"><%= shorturl(short) %></a></td>
|
|
36
|
+
</tr>
|
|
37
|
+
<% end %>
|
|
38
|
+
</tbody>
|
|
39
|
+
</table>
|
|
40
|
+
|
|
41
|
+
<h2>Most Hits</h2>
|
|
42
|
+
|
|
43
|
+
<table cellspacing="0">
|
|
44
|
+
<thead>
|
|
45
|
+
<tr>
|
|
46
|
+
<th>Hits</th>
|
|
47
|
+
<th>URL</th>
|
|
48
|
+
<th>Shortened</th>
|
|
49
|
+
</tr>
|
|
50
|
+
</thead>
|
|
51
|
+
|
|
52
|
+
<tbody>
|
|
53
|
+
<% @byhits.each do |short, data| %>
|
|
54
|
+
<tr>
|
|
55
|
+
<td><%= data[:score] %></td>
|
|
56
|
+
<td><a href="<%= data[:long] %>"><%= data[:long] %></a></td>
|
|
57
|
+
<td><a href="<%= shorturl(short) %>"><%= shorturl(short) %></a></td>
|
|
58
|
+
</tr>
|
|
59
|
+
<% end %>
|
|
60
|
+
</tbody>
|
|
61
|
+
</table>
|
|
62
|
+
<% end %>
|
data/lib/oso.rb
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
require "timeout"
|
|
2
|
+
require "net/http"
|
|
3
|
+
require "uri"
|
|
4
|
+
|
|
5
|
+
# A simple client for the Oso URL shortener.
|
|
6
|
+
|
|
7
|
+
class Oso
|
|
8
|
+
include Timeout
|
|
9
|
+
|
|
10
|
+
# Raised for any error.
|
|
11
|
+
|
|
12
|
+
Error = Class.new StandardError
|
|
13
|
+
|
|
14
|
+
# Duh.
|
|
15
|
+
|
|
16
|
+
VERSION = "1.0.0"
|
|
17
|
+
|
|
18
|
+
# :nodoc:
|
|
19
|
+
|
|
20
|
+
def self.instance
|
|
21
|
+
@instance ||= new
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# A static helper if you don't want to create new instances of the
|
|
25
|
+
# Oso class. Set the <tt>OSO_URL</tt> environment variable to
|
|
26
|
+
# control the server location. See +initialize+ for more
|
|
27
|
+
# information.
|
|
28
|
+
|
|
29
|
+
def self.shorten *args
|
|
30
|
+
instance.shorten(*args)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# A URI. Where does the Oso server live? If unspecified during
|
|
34
|
+
# initialization it'll default to the contents of the
|
|
35
|
+
# <tt>OSO_URL</tt> environment variable. If that's unset, it's
|
|
36
|
+
# <tt>"http://localhost:9292"</tt>.
|
|
37
|
+
|
|
38
|
+
attr_reader :url
|
|
39
|
+
|
|
40
|
+
# Create a new instance, optionally specifying the server +url+. See
|
|
41
|
+
# the +url+ property for defaults and fallbacks.
|
|
42
|
+
|
|
43
|
+
def initialize url = nil
|
|
44
|
+
url = url || ENV["OSO_URL"] || "http://localhost:9292"
|
|
45
|
+
url = "http://#{url}" unless /^http/ =~ url
|
|
46
|
+
|
|
47
|
+
@url = URI.parse url
|
|
48
|
+
@url.path = "/" if @url.path.empty?
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# Create a short URL for +url+. Pass a <tt>:life</tt> option
|
|
52
|
+
# (integer, in seconds) to control how long the short URL will be
|
|
53
|
+
# active. Pass a <tt>:limit</tt> option to control how many times
|
|
54
|
+
# the URL can be hit before it's deactivated. Pass a <tt>:name</tt>
|
|
55
|
+
# option to explicitly set the shortened URL.
|
|
56
|
+
#
|
|
57
|
+
# +shorten+ will raise an Oso::Error if network or server
|
|
58
|
+
# conditions keep +url+ from being shortened in one second.
|
|
59
|
+
|
|
60
|
+
def shorten url, options = {}
|
|
61
|
+
params = options.merge url: url
|
|
62
|
+
|
|
63
|
+
params[:life] &&= params[:life].to_i
|
|
64
|
+
params[:limit] &&= params[:limit].to_i
|
|
65
|
+
|
|
66
|
+
timeout 1, Oso::Error do
|
|
67
|
+
begin
|
|
68
|
+
res = Net::HTTP.post_form @url, params
|
|
69
|
+
rescue Errno::ECONNREFUSED
|
|
70
|
+
raise Oso::Error, "Connection to #@url refused."
|
|
71
|
+
rescue Timeout::Error
|
|
72
|
+
raise Oso::Error, "Connection to #@url timed out."
|
|
73
|
+
rescue Errno::EINVAL, Errno::ECONNRESET, EOFError => e
|
|
74
|
+
raise Oso::Error, "Connection to #@url failed. (#{e.class.name})"
|
|
75
|
+
rescue Net::HTTPBadResponse, Net::HTTPHeaderSyntaxError => e
|
|
76
|
+
raise Oso::Error, "#@url provided a bad response. (#{e.class.name})"
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
case res.code.to_i
|
|
80
|
+
when 201 then return res.body
|
|
81
|
+
else raise Oso::Error, "Unsuccessful shorten #{res.code}: #{res.body}"
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
end
|
data/test/test_oso.rb
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
require "minitest/autorun"
|
|
2
|
+
|
|
3
|
+
require "fakeweb"
|
|
4
|
+
require "mocha"
|
|
5
|
+
require "oso"
|
|
6
|
+
|
|
7
|
+
FakeWeb.allow_net_connect = false
|
|
8
|
+
|
|
9
|
+
class TestOso < MiniTest::Unit::TestCase
|
|
10
|
+
def setup
|
|
11
|
+
FakeWeb.clean_registry
|
|
12
|
+
ENV["OSO_URL"] = nil
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def test_initialize
|
|
16
|
+
assert_equal URI.parse("http://localhost:9292/"), Oso.new.url
|
|
17
|
+
|
|
18
|
+
ENV["OSO_URL"] = "http://foo/"
|
|
19
|
+
|
|
20
|
+
assert_equal URI.parse("http://foo/"), Oso.new.url
|
|
21
|
+
assert_equal URI.parse("http://foo/"), Oso.new("http://foo/").url
|
|
22
|
+
assert_equal URI.parse("http://foo/"), Oso.new("http://foo").url
|
|
23
|
+
assert_equal URI.parse("http://foo/"), Oso.new("foo").url
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def test_self_instance
|
|
27
|
+
assert_same Oso.instance, Oso.instance
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def test_self_shorten
|
|
31
|
+
Oso.instance.expects(:shorten).with("foo").returns "bar"
|
|
32
|
+
assert_equal "bar", Oso.shorten("foo")
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def test_shorten
|
|
36
|
+
FakeWeb.register_uri :post, "http://example.org/",
|
|
37
|
+
body: "http://example.org/1", status: 201
|
|
38
|
+
|
|
39
|
+
oso = Oso.new "example.org"
|
|
40
|
+
|
|
41
|
+
assert_equal "http://example.org/1", oso.shorten("whatever")
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def test_shorten_bad
|
|
45
|
+
FakeWeb.register_uri :post, "http://example.org/",
|
|
46
|
+
body: "No luck.", status: 404
|
|
47
|
+
|
|
48
|
+
oso = Oso.new "example.org"
|
|
49
|
+
|
|
50
|
+
assert_raises Oso::Error do
|
|
51
|
+
oso.shorten "blah"
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: oso
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
prerelease:
|
|
5
|
+
version: 1.0.0
|
|
6
|
+
platform: ruby
|
|
7
|
+
authors:
|
|
8
|
+
- Audiosocket
|
|
9
|
+
autorequire:
|
|
10
|
+
bindir: bin
|
|
11
|
+
cert_chain: []
|
|
12
|
+
|
|
13
|
+
date: 2011-02-17 00:00:00 -06:00
|
|
14
|
+
default_executable:
|
|
15
|
+
dependencies:
|
|
16
|
+
- !ruby/object:Gem::Dependency
|
|
17
|
+
name: fakeweb
|
|
18
|
+
prerelease: false
|
|
19
|
+
requirement: &id001 !ruby/object:Gem::Requirement
|
|
20
|
+
none: false
|
|
21
|
+
requirements:
|
|
22
|
+
- - "="
|
|
23
|
+
- !ruby/object:Gem::Version
|
|
24
|
+
version: 1.3.0
|
|
25
|
+
type: :development
|
|
26
|
+
version_requirements: *id001
|
|
27
|
+
- !ruby/object:Gem::Dependency
|
|
28
|
+
name: minitest
|
|
29
|
+
prerelease: false
|
|
30
|
+
requirement: &id002 !ruby/object:Gem::Requirement
|
|
31
|
+
none: false
|
|
32
|
+
requirements:
|
|
33
|
+
- - "="
|
|
34
|
+
- !ruby/object:Gem::Version
|
|
35
|
+
version: 2.0.2
|
|
36
|
+
type: :development
|
|
37
|
+
version_requirements: *id002
|
|
38
|
+
- !ruby/object:Gem::Dependency
|
|
39
|
+
name: mocha
|
|
40
|
+
prerelease: false
|
|
41
|
+
requirement: &id003 !ruby/object:Gem::Requirement
|
|
42
|
+
none: false
|
|
43
|
+
requirements:
|
|
44
|
+
- - "="
|
|
45
|
+
- !ruby/object:Gem::Version
|
|
46
|
+
version: 0.9.12
|
|
47
|
+
type: :development
|
|
48
|
+
version_requirements: *id003
|
|
49
|
+
- !ruby/object:Gem::Dependency
|
|
50
|
+
name: hoe
|
|
51
|
+
prerelease: false
|
|
52
|
+
requirement: &id004 !ruby/object:Gem::Requirement
|
|
53
|
+
none: false
|
|
54
|
+
requirements:
|
|
55
|
+
- - ">="
|
|
56
|
+
- !ruby/object:Gem::Version
|
|
57
|
+
version: 2.9.1
|
|
58
|
+
type: :development
|
|
59
|
+
version_requirements: *id004
|
|
60
|
+
description: |-
|
|
61
|
+
Audiosocket's minimal URL shortener. It's named "oso" because those
|
|
62
|
+
three letters look a bit like our logo.
|
|
63
|
+
email:
|
|
64
|
+
- it@audiosocket.com
|
|
65
|
+
executables: []
|
|
66
|
+
|
|
67
|
+
extensions: []
|
|
68
|
+
|
|
69
|
+
extra_rdoc_files:
|
|
70
|
+
- Manifest.txt
|
|
71
|
+
- lib/oso/public/robots.txt
|
|
72
|
+
- CHANGELOG.rdoc
|
|
73
|
+
- README.rdoc
|
|
74
|
+
files:
|
|
75
|
+
- .gems
|
|
76
|
+
- CHANGELOG.rdoc
|
|
77
|
+
- Manifest.txt
|
|
78
|
+
- README.rdoc
|
|
79
|
+
- Rakefile
|
|
80
|
+
- config.ru
|
|
81
|
+
- lib/oso.rb
|
|
82
|
+
- lib/oso/public/audiosocket.png
|
|
83
|
+
- lib/oso/public/favicon.ico
|
|
84
|
+
- lib/oso/public/index.css
|
|
85
|
+
- lib/oso/public/index.html
|
|
86
|
+
- lib/oso/public/robots.txt
|
|
87
|
+
- lib/oso/public/style.css
|
|
88
|
+
- lib/oso/server.rb
|
|
89
|
+
- lib/oso/views/layout.rhtml
|
|
90
|
+
- lib/oso/views/stats.rhtml
|
|
91
|
+
- test/test_oso.rb
|
|
92
|
+
- .gemtest
|
|
93
|
+
has_rdoc: true
|
|
94
|
+
homepage: http://github.com/audiosocket/oso
|
|
95
|
+
licenses: []
|
|
96
|
+
|
|
97
|
+
post_install_message:
|
|
98
|
+
rdoc_options:
|
|
99
|
+
- --main
|
|
100
|
+
- README.rdoc
|
|
101
|
+
require_paths:
|
|
102
|
+
- lib
|
|
103
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
104
|
+
none: false
|
|
105
|
+
requirements:
|
|
106
|
+
- - ">="
|
|
107
|
+
- !ruby/object:Gem::Version
|
|
108
|
+
version: "0"
|
|
109
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
110
|
+
none: false
|
|
111
|
+
requirements:
|
|
112
|
+
- - ">="
|
|
113
|
+
- !ruby/object:Gem::Version
|
|
114
|
+
version: "0"
|
|
115
|
+
requirements: []
|
|
116
|
+
|
|
117
|
+
rubyforge_project: oso
|
|
118
|
+
rubygems_version: 1.5.2
|
|
119
|
+
signing_key:
|
|
120
|
+
specification_version: 3
|
|
121
|
+
summary: Audiosocket's minimal URL shortener
|
|
122
|
+
test_files:
|
|
123
|
+
- test/test_oso.rb
|