oso 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|