firefly 0.2.0 → 0.3.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/API.md +86 -0
- data/HISTORY +11 -2
- data/README.md +39 -25
- data/VERSION +1 -1
- data/config.ru.example +3 -0
- data/firefly.gemspec +11 -4
- data/lib/firefly/base62.rb +1 -1
- data/lib/firefly/config.rb +4 -3
- data/lib/firefly/server.rb +27 -12
- data/lib/firefly/url.rb +10 -22
- data/public/style.css +9 -0
- data/spec/firefly/api_spec.rb +82 -0
- data/spec/firefly/base62_spec.rb +21 -0
- data/spec/firefly/server_spec.rb +50 -0
- data/spec/firefly/url_spec.rb +47 -0
- data/views/index.haml +32 -7
- data/views/info.haml +5 -2
- metadata +12 -5
- data/spec/firefly_spec.rb +0 -163
data/API.md
ADDED
@@ -0,0 +1,86 @@
|
|
1
|
+
# FireFly API
|
2
|
+
|
3
|
+
FireFly exposes its data via an Application Programming Interface (API), so developers can interact in a programmatic way with the FireFly website. This document is the official reference for that functionality. The current API version is 1.0
|
4
|
+
|
5
|
+
## Request / Response
|
6
|
+
|
7
|
+
FireFly responds in JSON.
|
8
|
+
|
9
|
+
A normal response may look like this:
|
10
|
+
|
11
|
+
{
|
12
|
+
"status_code": 200,
|
13
|
+
"data": {
|
14
|
+
"url": "http://example.com/cmeH01",
|
15
|
+
"hash": "cmeH01",
|
16
|
+
"long_url": "http://ariejan.net/",
|
17
|
+
"new_hash": 0
|
18
|
+
}
|
19
|
+
}
|
20
|
+
|
21
|
+
**Status codes**
|
22
|
+
|
23
|
+
FireFly sends back a `status_code`:
|
24
|
+
|
25
|
+
* 200 - OK
|
26
|
+
* 401 - Permission denied - wrong API key
|
27
|
+
* 404 - Hash or short URL not found (only for `expand` and `clicks`)
|
28
|
+
* 500 - Something bad happened.
|
29
|
+
|
30
|
+
## /v1/shorten
|
31
|
+
|
32
|
+
Shorten a long url to a short one.
|
33
|
+
|
34
|
+
GET /v1/shorten
|
35
|
+
|
36
|
+
**Parameters**
|
37
|
+
|
38
|
+
* `api_key` - Your API key, used for authentication
|
39
|
+
* `long_url` - The long URL. E.g. 'http://ariejan.net/'
|
40
|
+
|
41
|
+
**Output**
|
42
|
+
|
43
|
+
* `new_hash` - Is 1 if it's the first time the `long_url` is shortened, 0 otherwise.
|
44
|
+
* `url` - The short URL
|
45
|
+
* `hash` - The FireFly hash, this is a unique value
|
46
|
+
* `long_url` - Echos back the `long_url` that was shortened
|
47
|
+
|
48
|
+
## /v1/expand
|
49
|
+
|
50
|
+
Expand a short URL or hash to the long url.
|
51
|
+
|
52
|
+
GET /v1/expand
|
53
|
+
|
54
|
+
**Parameters**
|
55
|
+
|
56
|
+
* `short_url` - A valid short URL
|
57
|
+
* `hash` - A valid hash
|
58
|
+
* `api_key` - Your API key
|
59
|
+
|
60
|
+
_Note that either `short_url` or `hash` must be specified_
|
61
|
+
|
62
|
+
**Output**
|
63
|
+
|
64
|
+
* `short_url` - The short URL
|
65
|
+
* `hash` - The FireFly hash
|
66
|
+
* `long_url` - The original long URL
|
67
|
+
|
68
|
+
## /v1/clicks
|
69
|
+
|
70
|
+
Request the number of clicks of a specific short URL or hash.
|
71
|
+
|
72
|
+
GET /v1/clicks
|
73
|
+
|
74
|
+
**Parameters**
|
75
|
+
|
76
|
+
* `api_key` - Your API key
|
77
|
+
* `short_url` - A short URL
|
78
|
+
* `hash` - A hash
|
79
|
+
|
80
|
+
_Note that either `short_url` or `hash` must be specified_
|
81
|
+
|
82
|
+
**Output**
|
83
|
+
|
84
|
+
* `short_url` - The short URL
|
85
|
+
* `hash` - The FireFly hash
|
86
|
+
* `clicks` - The total number of clicks on this link
|
data/HISTORY
CHANGED
@@ -1,6 +1,15 @@
|
|
1
|
-
=
|
1
|
+
= 0.3.0 / 2010-04-13
|
2
|
+
|
3
|
+
* 2010-04-13 - Added recent urls to dashboard [ariejan]
|
4
|
+
- Added bookmarklet [ariejan]
|
5
|
+
- Split up Firefly codebase for better testing and maintainability [ariejan]
|
6
|
+
|
7
|
+
* 2010-04-02 - Added Ruby 1.9.x compatibility. [ariejan]
|
8
|
+
|
9
|
+
= 0.2.0 / 2010-03-29
|
2
10
|
|
3
11
|
* 2010-03-29 - Added some GUI sugar at '/'. [ariejan]
|
12
|
+
|
4
13
|
* 2010-03-29 - Added /api/info/:code for basic stats on the specified URL. [ariejan]
|
5
14
|
|
6
15
|
* 2010-03-29 - Keep count of the number of visits to a given URL. [ariejan]
|
@@ -11,4 +20,4 @@
|
|
11
20
|
|
12
21
|
= 0.1 / 2010-03-28
|
13
22
|
|
14
|
-
* First official release [ariejan]
|
23
|
+
* First official release [ariejan]
|
data/README.md
CHANGED
@@ -32,6 +32,8 @@ Next you can start your web server. You may try thin:
|
|
32
32
|
|
33
33
|
thin start -R config.ru
|
34
34
|
|
35
|
+
Now visit `http://:hostname/` and enter your `:api_key`. Happy URL shortening!
|
36
|
+
|
35
37
|
# Configuration
|
36
38
|
|
37
39
|
All configuration is done in `config.ru`.
|
@@ -46,6 +48,12 @@ It's possible to use all kinds of backends with DataMapper. Sqlite3 and MySQL ha
|
|
46
48
|
|
47
49
|
# Usage
|
48
50
|
|
51
|
+
Simply visit `http://:hostname/` and enter your `:api_key`. You can now shorten URLs.
|
52
|
+
|
53
|
+
## Advanced usage
|
54
|
+
|
55
|
+
You may also use the API to automate URL shortening. Here's how.
|
56
|
+
|
49
57
|
Adding a URL is done by doing a simple POST request that includes the URL and your API key.
|
50
58
|
|
51
59
|
curl -d "url=http://ariejan.net" -d "api_key=test" http://localhost:3000/api/add
|
@@ -72,34 +80,40 @@ After you restart Terminal.app (or at least reload the `.profile` file) you can
|
|
72
80
|
* [Source][2]
|
73
81
|
* [Issue tracker][3]
|
74
82
|
|
83
|
+
[2]: http://github.com/ariejan/firefly
|
84
|
+
[3]: http://github.com/ariejan/firefly/issues
|
85
|
+
|
75
86
|
Feel free to fork Firefly and create patches for it. Here are some basic instructions:
|
76
87
|
|
77
|
-
* Fork
|
78
|
-
*
|
79
|
-
*
|
80
|
-
*
|
81
|
-
*
|
88
|
+
* [Fork][4] Firefly
|
89
|
+
* Create a topic branch - `git checkout -b my_branch`
|
90
|
+
* Push to your branch - `git push origin my_branch`
|
91
|
+
* Create an [Issue][5] with a link to your branch
|
92
|
+
* That's it!
|
82
93
|
|
94
|
+
[4]: http://help.github.com/forking/
|
95
|
+
[5]: http://github.com/ariejan/firefly/issues
|
96
|
+
|
83
97
|
# License
|
84
98
|
|
85
|
-
Copyright (c) 2009 Ariejan de Vroom
|
86
|
-
|
87
|
-
Permission is hereby granted, free of charge, to any person obtaining
|
88
|
-
a copy of this software and associated documentation files (the
|
89
|
-
"Software"), to deal in the Software without restriction, including
|
90
|
-
without limitation the rights to use, copy, modify, merge, publish,
|
91
|
-
distribute, sublicense, and/or sell copies of the Software, and to
|
92
|
-
permit persons to whom the Software is furnished to do so, subject to
|
93
|
-
the following conditions:
|
94
|
-
|
95
|
-
The above copyright notice and this permission notice shall be
|
96
|
-
included in all copies or substantial portions of the Software.
|
97
|
-
|
98
|
-
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
99
|
-
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
100
|
-
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
101
|
-
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
102
|
-
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
103
|
-
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
104
|
-
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
99
|
+
Copyright (c) 2009 Ariejan de Vroom
|
100
|
+
|
101
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
102
|
+
a copy of this software and associated documentation files (the
|
103
|
+
"Software"), to deal in the Software without restriction, including
|
104
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
105
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
106
|
+
permit persons to whom the Software is furnished to do so, subject to
|
107
|
+
the following conditions:
|
108
|
+
|
109
|
+
The above copyright notice and this permission notice shall be
|
110
|
+
included in all copies or substantial portions of the Software.
|
111
|
+
|
112
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
113
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
114
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
115
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
116
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
117
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
118
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
105
119
|
|
data/VERSION
CHANGED
@@ -1 +1 @@
|
|
1
|
-
0.
|
1
|
+
0.3.0
|
data/config.ru.example
CHANGED
@@ -10,6 +10,9 @@ app = Firefly::Server.new do
|
|
10
10
|
# Use Sqlite3 by default
|
11
11
|
set :database, "sqlite3://#{Dir.pwd}/firefly.sqlite3"
|
12
12
|
|
13
|
+
# Set number of recent urls to show
|
14
|
+
set :recent_urls, 10
|
15
|
+
|
13
16
|
# You can use MySQL as well.
|
14
17
|
# Make sure to install the do_mysql gem:
|
15
18
|
# sudo gem install do_mysql
|
data/firefly.gemspec
CHANGED
@@ -5,11 +5,11 @@
|
|
5
5
|
|
6
6
|
Gem::Specification.new do |s|
|
7
7
|
s.name = %q{firefly}
|
8
|
-
s.version = "0.
|
8
|
+
s.version = "0.3.0"
|
9
9
|
|
10
10
|
s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
|
11
11
|
s.authors = ["Ariejan de Vroom"]
|
12
|
-
s.date = %q{2010-
|
12
|
+
s.date = %q{2010-04-13}
|
13
13
|
s.description = %q{FireFly is a simple URL shortner for personal use. It's powered by Sinatra and can be run with any Rack-capable web server.}
|
14
14
|
s.email = %q{ariejan@ariejan.net}
|
15
15
|
s.extra_rdoc_files = [
|
@@ -18,6 +18,7 @@ Gem::Specification.new do |s|
|
|
18
18
|
]
|
19
19
|
s.files = [
|
20
20
|
".gitignore",
|
21
|
+
"API.md",
|
21
22
|
"HISTORY",
|
22
23
|
"LICENSE",
|
23
24
|
"README.md",
|
@@ -35,7 +36,10 @@ Gem::Specification.new do |s|
|
|
35
36
|
"public/jquery-1.4.2.min.js",
|
36
37
|
"public/reset.css",
|
37
38
|
"public/style.css",
|
38
|
-
"spec/
|
39
|
+
"spec/firefly/api_spec.rb",
|
40
|
+
"spec/firefly/base62_spec.rb",
|
41
|
+
"spec/firefly/server_spec.rb",
|
42
|
+
"spec/firefly/url_spec.rb",
|
39
43
|
"spec/spec.opts",
|
40
44
|
"spec/spec_helper.rb",
|
41
45
|
"views/index.haml",
|
@@ -49,7 +53,10 @@ Gem::Specification.new do |s|
|
|
49
53
|
s.rubygems_version = %q{1.3.6}
|
50
54
|
s.summary = %q{FireFly is a simple URL shortner for personal use}
|
51
55
|
s.test_files = [
|
52
|
-
"spec/
|
56
|
+
"spec/firefly/api_spec.rb",
|
57
|
+
"spec/firefly/base62_spec.rb",
|
58
|
+
"spec/firefly/server_spec.rb",
|
59
|
+
"spec/firefly/url_spec.rb",
|
53
60
|
"spec/spec_helper.rb"
|
54
61
|
]
|
55
62
|
|
data/lib/firefly/base62.rb
CHANGED
data/lib/firefly/config.rb
CHANGED
@@ -2,9 +2,10 @@ module Firefly
|
|
2
2
|
class Config < Hash
|
3
3
|
|
4
4
|
DEFAULTS = {
|
5
|
-
:hostname
|
6
|
-
:api_key
|
7
|
-
:database
|
5
|
+
:hostname => "localhost:3000",
|
6
|
+
:api_key => "test",
|
7
|
+
:database => "sqlite3://#{Dir.pwd}/firefly_#{ENV['RACK_ENV']}.sqlite3",
|
8
|
+
:recent_urls => 25
|
8
9
|
}
|
9
10
|
|
10
11
|
def initialize obj
|
data/lib/firefly/server.rb
CHANGED
@@ -44,11 +44,16 @@ module Firefly
|
|
44
44
|
end
|
45
45
|
end
|
46
46
|
|
47
|
+
def short_url(url)
|
48
|
+
"http://#{config[:hostname]}/#{url.code}"
|
49
|
+
end
|
50
|
+
|
47
51
|
def generate_short_url(url = nil)
|
48
|
-
|
52
|
+
code, result = nil, nil
|
49
53
|
|
50
54
|
if !url.nil? && url != ""
|
51
|
-
|
55
|
+
ff_url = Firefly::Url.shorten(url)
|
56
|
+
code = ff_url.code
|
52
57
|
result = "http://#{config[:hostname]}/#{code}"
|
53
58
|
end
|
54
59
|
|
@@ -62,6 +67,7 @@ module Firefly
|
|
62
67
|
end
|
63
68
|
|
64
69
|
get '/' do
|
70
|
+
@urls = Firefly::Url.all(:limit => config[:recent_urls], :order => [ :created_at.desc ])
|
65
71
|
haml :index
|
66
72
|
end
|
67
73
|
|
@@ -76,12 +82,13 @@ module Firefly
|
|
76
82
|
redirect '/'
|
77
83
|
end
|
78
84
|
|
85
|
+
# GET /add?url=http://ariejan.net&api_key=test
|
79
86
|
# POST /add?url=http://ariejan.net&api_key=test
|
80
87
|
#
|
81
88
|
# Returns the shortened URL
|
82
|
-
|
89
|
+
api_add = lambda {
|
83
90
|
validate_api_permission
|
84
|
-
@url
|
91
|
+
@url = params[:url]
|
85
92
|
@code, @result = generate_short_url(@url)
|
86
93
|
@result ||= "Invalid URL specified."
|
87
94
|
|
@@ -90,7 +97,10 @@ module Firefly
|
|
90
97
|
else
|
91
98
|
@result
|
92
99
|
end
|
93
|
-
|
100
|
+
}
|
101
|
+
|
102
|
+
get '/api/add', &api_add
|
103
|
+
post '/api/add', &api_add
|
94
104
|
|
95
105
|
# GET /b3d+
|
96
106
|
#
|
@@ -98,24 +108,29 @@ module Firefly
|
|
98
108
|
get '/api/info/:code' do
|
99
109
|
validate_api_permission
|
100
110
|
|
101
|
-
@url = Firefly::Url.
|
102
|
-
@short_url = "http://#{config[:hostname]}/#{@url.code}"
|
111
|
+
@url = Firefly::Url.first(:code => params[:code])
|
103
112
|
|
104
|
-
|
113
|
+
if @url.nil?
|
114
|
+
status 404
|
115
|
+
"Sorry, that code is unknown."
|
116
|
+
else
|
117
|
+
@short_url = "http://#{config[:hostname]}/#{@url.code}"
|
118
|
+
haml :info
|
119
|
+
end
|
105
120
|
end
|
106
121
|
|
107
122
|
# GET /b3d
|
108
123
|
#
|
109
124
|
# Redirect to the shortened URL
|
110
125
|
get '/:code' do
|
111
|
-
url = Firefly::Url.
|
126
|
+
@url = Firefly::Url.first(:code => params[:code])
|
112
127
|
|
113
|
-
if url.nil?
|
128
|
+
if @url.nil?
|
114
129
|
status 404
|
115
130
|
"Sorry, that code is unknown."
|
116
131
|
else
|
117
|
-
url.
|
118
|
-
redirect url.url, 301
|
132
|
+
@url.register_click!
|
133
|
+
redirect @url.url, 301
|
119
134
|
end
|
120
135
|
end
|
121
136
|
|
data/lib/firefly/url.rb
CHANGED
@@ -4,32 +4,20 @@ module Firefly
|
|
4
4
|
|
5
5
|
property :id, Serial
|
6
6
|
property :url, String, :index => true, :length => 255
|
7
|
-
property :code, String, :index => true, :length =>
|
8
|
-
property :
|
7
|
+
property :code, String, :index => true, :length => 16
|
8
|
+
property :clicks, Integer, :default => 0
|
9
9
|
property :created_at, DateTime, :default => Time.now
|
10
10
|
|
11
11
|
# Increase the visits counter by 1
|
12
|
-
def
|
13
|
-
self.update(:
|
12
|
+
def register_click!
|
13
|
+
self.update(:clicks => self.clicks + 1)
|
14
14
|
end
|
15
15
|
|
16
|
-
#
|
17
|
-
def self.
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
@result = self.create(:url => url)
|
23
|
-
@result.update(:code => Firefly::Base62.encode(@result.id.to_i))
|
24
|
-
end
|
25
|
-
|
26
|
-
return @result.code
|
27
|
-
end
|
28
|
-
|
29
|
-
# Decode a code to the original URL
|
30
|
-
def self.decode(code)
|
31
|
-
@result = Firefly::Url.first(:code => code)
|
32
|
-
return @result.nil? ? nil : @result
|
33
|
-
end
|
16
|
+
# Shorten a long_url and return a new FireFly::Url
|
17
|
+
def self.shorten(long_url)
|
18
|
+
the_url = Firefly::Url.first(:url => long_url) || Firefly::Url.create(:url => long_url)
|
19
|
+
the_url.update(:code => Firefly::Base62.encode(the_url.id.to_i)) if the_url.code.nil?
|
20
|
+
the_url
|
21
|
+
end
|
34
22
|
end
|
35
23
|
end
|
data/public/style.css
CHANGED
@@ -73,3 +73,12 @@ body { padding:0; margin:0; }
|
|
73
73
|
#main p.pagination a.more { float:right;}
|
74
74
|
|
75
75
|
#main form.clear-failed {float:right; margin-top:-10px;}
|
76
|
+
|
77
|
+
#main input.big_url { font-size: 1.8em; padding: 0.2em; }
|
78
|
+
|
79
|
+
#main h2 { color: #ce1212;}
|
80
|
+
|
81
|
+
#main .sidebox { float: right; width: 320px; }
|
82
|
+
|
83
|
+
#main .the_form { margin-bottom: 30px; }
|
84
|
+
#main table tr td.label { font-weight: bold; }
|
@@ -0,0 +1,82 @@
|
|
1
|
+
require File.expand_path(File.dirname(__FILE__) + '/../spec_helper')
|
2
|
+
|
3
|
+
describe "API" do
|
4
|
+
include Rack::Test::Methods
|
5
|
+
|
6
|
+
def app
|
7
|
+
@@app
|
8
|
+
end
|
9
|
+
|
10
|
+
[:post, :get].each do |verb|
|
11
|
+
describe "adding a URL by #{verb.to_s.upcase}" do
|
12
|
+
it "should be okay adding a new URL" do
|
13
|
+
self.send verb, '/api/add', :url => 'http://example.org', :api_key => 'test'
|
14
|
+
last_response.should be_ok
|
15
|
+
end
|
16
|
+
|
17
|
+
it "should return the shortened URL" do
|
18
|
+
self.send verb, '/api/add', :url => 'http://example.org', :api_key => 'test'
|
19
|
+
|
20
|
+
url = Firefly::Url.first(:url => "http://example.org")
|
21
|
+
|
22
|
+
last_response.body.should eql("http://test.host/#{url.code}")
|
23
|
+
end
|
24
|
+
|
25
|
+
it "should return a 401 on a wrong API key" do
|
26
|
+
self.send verb, '/api/add', :url => 'http://example.org', :api_key => 'false'
|
27
|
+
last_response.status.should eql(401)
|
28
|
+
end
|
29
|
+
|
30
|
+
it "should create a new Firefly::Url" do
|
31
|
+
lambda {
|
32
|
+
self.send verb, '/api/add', :url => 'http://example.org', :api_key => 'test'
|
33
|
+
}.should change(Firefly::Url, :count).by(1)
|
34
|
+
end
|
35
|
+
|
36
|
+
it "should not create the same Firefly::Url twice" do
|
37
|
+
self.send verb, '/api/add', :url => 'http://example.org', :api_key => 'test'
|
38
|
+
|
39
|
+
lambda {
|
40
|
+
self.send verb, '/api/add', :url => 'http://example.org', :api_key => 'test'
|
41
|
+
}.should_not change(Firefly::Url, :count).by(1)
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
describe "getting information" do
|
47
|
+
before(:each) do
|
48
|
+
@created_at = Time.now
|
49
|
+
@url = Firefly::Url.create(:url => 'http://example.com/123', :code => 'alpha', :clicks => 69, :created_at => @created_at)
|
50
|
+
end
|
51
|
+
|
52
|
+
it "should work" do
|
53
|
+
get '/api/info/alpha', :api_key => "test"
|
54
|
+
last_response.should be_ok
|
55
|
+
end
|
56
|
+
|
57
|
+
it "should show the click count" do
|
58
|
+
get '/api/info/alpha', :api_key => "test"
|
59
|
+
last_response.body.should match(/69/)
|
60
|
+
end
|
61
|
+
|
62
|
+
it "should show the short URL" do
|
63
|
+
get '/api/info/alpha', :api_key => "test"
|
64
|
+
last_response.body.should match(/alpha/)
|
65
|
+
end
|
66
|
+
|
67
|
+
it "should show the shortened at time" do
|
68
|
+
get '/api/info/alpha', :api_key => "test"
|
69
|
+
last_response.body.should match(/#{@created_at.strftime("%Y-%m-%d %H:%M")}/)
|
70
|
+
end
|
71
|
+
|
72
|
+
it "should show the full URL" do
|
73
|
+
get '/api/info/alpha', :api_key => "test"
|
74
|
+
last_response.body.should match(/http:\/\/example.com\/123/)
|
75
|
+
end
|
76
|
+
|
77
|
+
it "should validate API permissions" do
|
78
|
+
get '/api/info/alpha', :api_key => false
|
79
|
+
last_response.status.should be(401)
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
require File.expand_path(File.dirname(__FILE__) + '/../spec_helper')
|
2
|
+
|
3
|
+
describe "Base62 encoding/decoding" do
|
4
|
+
[
|
5
|
+
[ 1, "1"],
|
6
|
+
[ 10, "a"],
|
7
|
+
[ 61, "Z"],
|
8
|
+
[ 62, "10"],
|
9
|
+
[ 63, "11"],
|
10
|
+
[ 124, "20"],
|
11
|
+
[200000000, "dxb8s"]
|
12
|
+
].each do |input, output|
|
13
|
+
it "should encode #{input} correctly to #{output}" do
|
14
|
+
Firefly::Base62.encode(input).should eql(output)
|
15
|
+
end
|
16
|
+
|
17
|
+
it "should decode correctly" do
|
18
|
+
Firefly::Base62.decode(output).should eql(input)
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,50 @@
|
|
1
|
+
require File.expand_path(File.dirname(__FILE__) + '/../spec_helper')
|
2
|
+
|
3
|
+
describe "Firefly" do
|
4
|
+
include Rack::Test::Methods
|
5
|
+
|
6
|
+
def app
|
7
|
+
@@app
|
8
|
+
end
|
9
|
+
|
10
|
+
describe "/" do
|
11
|
+
it "should respond ok" do
|
12
|
+
get '/'
|
13
|
+
last_response.should be_ok
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
describe "redirecting" do
|
18
|
+
it "should redirect to the original URL" do
|
19
|
+
fake = Firefly::Url.create(:url => 'http://example.com/123', :code => 'alpha')
|
20
|
+
|
21
|
+
get '/alpha'
|
22
|
+
follow_redirect!
|
23
|
+
|
24
|
+
last_request.url.should eql('http://example.com/123')
|
25
|
+
end
|
26
|
+
|
27
|
+
it "should increase the visits counter" do
|
28
|
+
fake = Firefly::Url.create(:url => 'http://example.com/123', :code => 'alpha')
|
29
|
+
Firefly::Url.should_receive(:first).and_return(fake)
|
30
|
+
|
31
|
+
lambda {
|
32
|
+
get '/alpha'
|
33
|
+
}.should change(fake, :clicks).by(1)
|
34
|
+
end
|
35
|
+
|
36
|
+
it "should redirect with a 301 Permanent redirect" do
|
37
|
+
fake = Firefly::Url.create(:url => 'http://example.com/123', :code => 'alpha')
|
38
|
+
|
39
|
+
get '/alpha'
|
40
|
+
|
41
|
+
last_response.status.should eql(301)
|
42
|
+
end
|
43
|
+
|
44
|
+
it "should throw a 404 when the code is unknown" do
|
45
|
+
get '/some_random_code_that_does_not_exist'
|
46
|
+
|
47
|
+
last_response.status.should eql(404)
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
@@ -0,0 +1,47 @@
|
|
1
|
+
require File.expand_path(File.dirname(__FILE__) + '/../spec_helper')
|
2
|
+
|
3
|
+
describe "Url" do
|
4
|
+
|
5
|
+
describe "shortening" do
|
6
|
+
|
7
|
+
it "should generate a code after create" do
|
8
|
+
url = Firefly::Url.shorten("http://example.com/")
|
9
|
+
Firefly::Url.first(:url => "http://example.com/").code.should_not be_nil
|
10
|
+
end
|
11
|
+
|
12
|
+
it "should set a clicks count of 0 for newly shortened urls" do
|
13
|
+
url = Firefly::Url.shorten("http://example.com/")
|
14
|
+
Firefly::Url.first(:url => "http://example.com/").clicks.should eql(0)
|
15
|
+
end
|
16
|
+
|
17
|
+
it "should create a new Firefly::Url with a new long_url" do
|
18
|
+
lambda {
|
19
|
+
Firefly::Url.shorten("http://example.com/")
|
20
|
+
}.should change(Firefly::Url, :count).by(1)
|
21
|
+
end
|
22
|
+
|
23
|
+
it "should return an existing Firefly::Url if the long_url exists" do
|
24
|
+
Firefly::Url.shorten("http://example.com/")
|
25
|
+
lambda {
|
26
|
+
Firefly::Url.shorten("http://example.com/")
|
27
|
+
}.should_not change(Firefly::Url, :count)
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
describe "clicking" do
|
32
|
+
before(:each) do
|
33
|
+
Firefly::Url.create(
|
34
|
+
:url => 'http://example.com/123',
|
35
|
+
:code => 'alpha',
|
36
|
+
:clicks => 69
|
37
|
+
)
|
38
|
+
@url = Firefly::Url.first(:code => 'alpha')
|
39
|
+
end
|
40
|
+
|
41
|
+
it "should increase the click count" do
|
42
|
+
lambda {
|
43
|
+
@url.register_click!
|
44
|
+
}.should change(@url, :clicks).by(1)
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
data/views/index.haml
CHANGED
@@ -1,14 +1,39 @@
|
|
1
1
|
- if @authenticated
|
2
|
-
|
2
|
+
|
3
|
+
.sidebox
|
4
|
+
%h2 Bookmarklet
|
3
5
|
|
4
|
-
%form{ :action => '/api/add', :method => 'post' }
|
5
|
-
%input{ :type => "hidden", :name => "visual", :value => "1" }
|
6
6
|
%p
|
7
|
-
|
8
|
-
%input{ :type => 'text', :name => 'url', :id => 'url' }
|
7
|
+
Drag the following link to your bookmarks. Click it to shorten the URL to the page you're currently viewing.
|
9
8
|
%p
|
10
|
-
%
|
11
|
-
|
9
|
+
%a{ :href => "javascript:var%20d=document,w=window,enc=encodeURIComponent,e=w.getSelection,k=d.getSelection,x=d.selection,s=(e?e():(k)?k():(x?x.createRange().text:0)),s2=((s.toString()=='')?s:('%22'+enc(s)+'%22')),f='http://#{@config[:hostname]}/api/add',l=d.location,p='?visual=1&api_key=#{@config[:api_key]}&url='+enc(l.href),u=f+p;try{if(!/^(.*\.)?tumblrzzz[^.]*$/.test(l.host))throw(0);tstbklt();}catch(z){a%20=function(){if(!w.open(u))l.href=u;};if(/Firefox/.test(navigator.userAgent))setTimeout(a,0);else%20a();}void(0)" } Shorten with #{@config[:hostname]}
|
10
|
+
|
11
|
+
%h1 How about shortening a URL?
|
12
|
+
|
13
|
+
.the_form
|
14
|
+
%form{ :action => '/api/add', :method => 'post' }
|
15
|
+
%input{ :type => "hidden", :name => "visual", :value => "1" }
|
16
|
+
%p
|
17
|
+
%input.big_url{ :type => 'text', :name => 'url', :id => 'url', :size => 50 }
|
18
|
+
%p
|
19
|
+
%input.big_url{ :type => 'submit', :name => 'submit', :id => 'submit', :value => "Make it short!" }
|
20
|
+
|
21
|
+
%h2 Recently shortened
|
22
|
+
|
23
|
+
%table
|
24
|
+
%tr
|
25
|
+
%td.label Short URL
|
26
|
+
%td.label Full URL
|
27
|
+
%td.label Clicks
|
28
|
+
%td.label Shortened at
|
29
|
+
- @urls.each do |url|
|
30
|
+
%tr
|
31
|
+
%td.value #{short_url(url)}
|
32
|
+
%td.value <a href="#{url.url}">#{url.url}</a>
|
33
|
+
%td.value= url.clicks
|
34
|
+
%td.value= url.created_at.strftime("%Y-%m-%d %H:%M")
|
35
|
+
|
36
|
+
|
12
37
|
- else
|
13
38
|
%h1 Please enter your API key
|
14
39
|
|
data/views/info.haml
CHANGED
@@ -1,15 +1,18 @@
|
|
1
|
+
%p.link
|
2
|
+
%a{ :href => "/" } « Back
|
3
|
+
|
1
4
|
%h1 http://#{@config[:hostname]}/#{@url.code}
|
2
5
|
|
3
6
|
%table
|
4
7
|
%tr
|
5
8
|
%td.label Short URL
|
6
|
-
%td.value
|
9
|
+
%td.value #{@short_url}
|
7
10
|
%tr
|
8
11
|
%td.label Full URL
|
9
12
|
%td.value <a href="#{@url.url}">#{@url.url}</a>
|
10
13
|
%tr
|
11
14
|
%td.label Clicks
|
12
|
-
%td.value= @url.
|
15
|
+
%td.value= @url.clicks
|
13
16
|
%tr
|
14
17
|
%td.label Shortened at
|
15
18
|
%td.value= @url.created_at.strftime("%Y-%m-%d %H:%M")
|
metadata
CHANGED
@@ -4,9 +4,9 @@ version: !ruby/object:Gem::Version
|
|
4
4
|
prerelease: false
|
5
5
|
segments:
|
6
6
|
- 0
|
7
|
-
-
|
7
|
+
- 3
|
8
8
|
- 0
|
9
|
-
version: 0.
|
9
|
+
version: 0.3.0
|
10
10
|
platform: ruby
|
11
11
|
authors:
|
12
12
|
- Ariejan de Vroom
|
@@ -14,7 +14,7 @@ autorequire:
|
|
14
14
|
bindir: bin
|
15
15
|
cert_chain: []
|
16
16
|
|
17
|
-
date: 2010-
|
17
|
+
date: 2010-04-13 00:00:00 +02:00
|
18
18
|
default_executable:
|
19
19
|
dependencies:
|
20
20
|
- !ruby/object:Gem::Dependency
|
@@ -126,6 +126,7 @@ extra_rdoc_files:
|
|
126
126
|
- README.md
|
127
127
|
files:
|
128
128
|
- .gitignore
|
129
|
+
- API.md
|
129
130
|
- HISTORY
|
130
131
|
- LICENSE
|
131
132
|
- README.md
|
@@ -143,7 +144,10 @@ files:
|
|
143
144
|
- public/jquery-1.4.2.min.js
|
144
145
|
- public/reset.css
|
145
146
|
- public/style.css
|
146
|
-
- spec/
|
147
|
+
- spec/firefly/api_spec.rb
|
148
|
+
- spec/firefly/base62_spec.rb
|
149
|
+
- spec/firefly/server_spec.rb
|
150
|
+
- spec/firefly/url_spec.rb
|
147
151
|
- spec/spec.opts
|
148
152
|
- spec/spec_helper.rb
|
149
153
|
- views/index.haml
|
@@ -180,5 +184,8 @@ signing_key:
|
|
180
184
|
specification_version: 3
|
181
185
|
summary: FireFly is a simple URL shortner for personal use
|
182
186
|
test_files:
|
183
|
-
- spec/
|
187
|
+
- spec/firefly/api_spec.rb
|
188
|
+
- spec/firefly/base62_spec.rb
|
189
|
+
- spec/firefly/server_spec.rb
|
190
|
+
- spec/firefly/url_spec.rb
|
184
191
|
- spec/spec_helper.rb
|
data/spec/firefly_spec.rb
DELETED
@@ -1,163 +0,0 @@
|
|
1
|
-
require File.expand_path(File.dirname(__FILE__) + '/spec_helper')
|
2
|
-
|
3
|
-
require File.dirname(__FILE__) + '/spec_helper'
|
4
|
-
|
5
|
-
describe "Firefly" do
|
6
|
-
include Rack::Test::Methods
|
7
|
-
|
8
|
-
def app
|
9
|
-
@@app
|
10
|
-
end
|
11
|
-
|
12
|
-
describe "/" do
|
13
|
-
it "should respond ok" do
|
14
|
-
get '/'
|
15
|
-
last_response.should be_ok
|
16
|
-
end
|
17
|
-
end
|
18
|
-
|
19
|
-
describe "Url" do
|
20
|
-
before(:each) do
|
21
|
-
@url = Firefly::Url.create(:url => 'http://example.com/123', :code => 'alpha')
|
22
|
-
end
|
23
|
-
|
24
|
-
it "should set a created_at timestamp" do
|
25
|
-
@url.created_at.should_not be_nil
|
26
|
-
end
|
27
|
-
|
28
|
-
it "should have a visit count of 0 by default" do
|
29
|
-
@url.visits.should eql(0)
|
30
|
-
end
|
31
|
-
|
32
|
-
it "should increase visits when calling visit!" do
|
33
|
-
lambda {
|
34
|
-
@url.visit!
|
35
|
-
}.should change(@url, :visits).by(1)
|
36
|
-
end
|
37
|
-
end
|
38
|
-
|
39
|
-
describe "adding a URL" do
|
40
|
-
it "should be okay adding a new URL" do
|
41
|
-
post '/api/add', :url => 'http://example.org', :api_key => 'test'
|
42
|
-
last_response.should be_ok
|
43
|
-
end
|
44
|
-
|
45
|
-
it "should return the shortened URL" do
|
46
|
-
post '/api/add', :url => 'http://example.org', :api_key => 'test'
|
47
|
-
|
48
|
-
url = Firefly::Url.first(:url => "http://example.org")
|
49
|
-
|
50
|
-
last_response.body.should eql("http://test.host/#{url.code}")
|
51
|
-
end
|
52
|
-
|
53
|
-
it "should return a 401 on a wrong API key" do
|
54
|
-
post '/api/add', :url => 'http://example.org', :api_key => 'false'
|
55
|
-
last_response.status.should eql(401)
|
56
|
-
end
|
57
|
-
|
58
|
-
it "should create a new Firefly::Url" do
|
59
|
-
lambda {
|
60
|
-
post '/api/add', :url => 'http://example.org', :api_key => 'test'
|
61
|
-
}.should change(Firefly::Url, :count).by(1)
|
62
|
-
end
|
63
|
-
|
64
|
-
it "should not create the same Firefly::Url twice" do
|
65
|
-
post '/api/add', :url => 'http://example.org', :api_key => 'test'
|
66
|
-
|
67
|
-
lambda {
|
68
|
-
post '/api/add', :url => 'http://example.org', :api_key => 'test'
|
69
|
-
}.should_not change(Firefly::Url, :count).by(1)
|
70
|
-
end
|
71
|
-
end
|
72
|
-
|
73
|
-
describe "Base62 encoding/decoding" do
|
74
|
-
[
|
75
|
-
[ 1, "1"],
|
76
|
-
[ 10, "a"],
|
77
|
-
[ 61, "Z"],
|
78
|
-
[ 62, "10"],
|
79
|
-
[ 63, "11"],
|
80
|
-
[ 124, "20"],
|
81
|
-
[200000000, "dxb8s"]
|
82
|
-
].each do |input, output|
|
83
|
-
it "should encode #{input} correctly to #{output}" do
|
84
|
-
Firefly::Base62.encode(input).should eql(output)
|
85
|
-
end
|
86
|
-
|
87
|
-
it "should decode correctly" do
|
88
|
-
Firefly::Base62.decode(output).should eql(input)
|
89
|
-
end
|
90
|
-
end
|
91
|
-
end
|
92
|
-
|
93
|
-
describe "getting information" do
|
94
|
-
before(:each) do
|
95
|
-
@created_at = Time.now
|
96
|
-
@url = Firefly::Url.create(:url => 'http://example.com/123', :code => 'alpha', :visits => 69, :created_at => @created_at)
|
97
|
-
end
|
98
|
-
|
99
|
-
it "should work" do
|
100
|
-
get '/api/info/alpha', :api_key => "test"
|
101
|
-
last_response.should be_ok
|
102
|
-
end
|
103
|
-
|
104
|
-
it "should show the visit count" do
|
105
|
-
get '/api/info/alpha', :api_key => "test"
|
106
|
-
last_response.body.should match(/69/)
|
107
|
-
end
|
108
|
-
|
109
|
-
it "should show the short URL" do
|
110
|
-
get '/api/info/alpha', :api_key => "test"
|
111
|
-
last_response.body.should match(/alpha/)
|
112
|
-
end
|
113
|
-
|
114
|
-
it "should show the shortened at time" do
|
115
|
-
get '/api/info/alpha', :api_key => "test"
|
116
|
-
last_response.body.should match(/#{@created_at.strftime("%Y-%m-%d %H:%M")}/)
|
117
|
-
end
|
118
|
-
|
119
|
-
it "should show the full URL" do
|
120
|
-
get '/api/info/alpha', :api_key => "test"
|
121
|
-
last_response.body.should match(/http:\/\/example.com\/123/)
|
122
|
-
end
|
123
|
-
|
124
|
-
it "should validate API permissions" do
|
125
|
-
get '/api/info/alpha', :api_key => false
|
126
|
-
last_response.status.should be(401)
|
127
|
-
end
|
128
|
-
end
|
129
|
-
|
130
|
-
describe "redirecting" do
|
131
|
-
it "should redirect to the original URL" do
|
132
|
-
fake = Firefly::Url.create(:url => 'http://example.com/123', :code => 'alpha')
|
133
|
-
|
134
|
-
get '/alpha'
|
135
|
-
follow_redirect!
|
136
|
-
|
137
|
-
last_request.url.should eql('http://example.com/123')
|
138
|
-
end
|
139
|
-
|
140
|
-
it "should increase the visits counter" do
|
141
|
-
fake = Firefly::Url.create(:url => 'http://example.com/123', :code => 'alpha')
|
142
|
-
Firefly::Url.should_receive(:decode).and_return(fake)
|
143
|
-
|
144
|
-
lambda {
|
145
|
-
get '/alpha'
|
146
|
-
}.should change(fake, :visits).by(1)
|
147
|
-
end
|
148
|
-
|
149
|
-
it "should redirect with a 301 Permanent redirect" do
|
150
|
-
fake = Firefly::Url.create(:url => 'http://example.com/123', :code => 'alpha')
|
151
|
-
|
152
|
-
get '/alpha'
|
153
|
-
|
154
|
-
last_response.status.should eql(301)
|
155
|
-
end
|
156
|
-
|
157
|
-
it "should throw a 404 when the code is unknown" do
|
158
|
-
get '/some_random_code_that_does_not_exist'
|
159
|
-
|
160
|
-
last_response.status.should eql(404)
|
161
|
-
end
|
162
|
-
end
|
163
|
-
end
|