guillotine 1.1.0 → 1.2.0
Sign up to get free protection for your applications and to get access to all the features.
- data/CHANGELOG.md +12 -0
- data/README.md +51 -7
- data/guillotine.gemspec +5 -2
- data/lib/guillotine.rb +55 -40
- data/lib/guillotine/adapters/active_record_adapter.rb +62 -64
- data/lib/guillotine/adapters/memory_adapter.rb +53 -51
- data/lib/guillotine/adapters/mongo_adapter.rb +56 -58
- data/lib/guillotine/adapters/redis_adapter.rb +58 -52
- data/lib/guillotine/adapters/riak_adapter.rb +107 -109
- data/lib/guillotine/adapters/sequel_adapter.rb +61 -63
- data/lib/guillotine/host_checkers.rb +87 -0
- data/lib/guillotine/service.rb +6 -51
- data/test/active_record_adapter_test.rb +1 -1
- data/test/app_test.rb +1 -1
- data/test/host_checker_test.rb +114 -0
- data/test/memory_adapter_test.rb +1 -1
- data/test/mongo_adapter_test.rb +1 -1
- data/test/redis_adapter_test.rb +1 -1
- data/test/riak_adapter_test.rb +3 -3
- data/test/sequel_adapter_test.rb +1 -1
- data/test/service_test.rb +1 -1
- metadata +12 -11
data/CHANGELOG.md
ADDED
@@ -0,0 +1,12 @@
|
|
1
|
+
# Changelog
|
2
|
+
|
3
|
+
## master
|
4
|
+
|
5
|
+
* Add a simple wildcard host checker.
|
6
|
+
|
7
|
+
Guillotine::Service.new(adapter, '*.foo.com')
|
8
|
+
|
9
|
+
* Guillotine::Adapters has been deprecated until v2. Adapters are now in the
|
10
|
+
top level namespace.
|
11
|
+
* Memory Adapter can be reset in tests
|
12
|
+
|
data/README.md
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
# Guillotine
|
2
2
|
|
3
|
-
Simple URL Shortener hobby kit.
|
3
|
+
Simple URL Shortener hobby kit. Currently used to shorten URLs at GitHub.com, and also available as a an [installable Heroku app](https://github.com/mrtazz/katana).
|
4
4
|
|
5
5
|
## USAGE
|
6
6
|
|
@@ -11,7 +11,8 @@ The easiest way to use it is with the built-in memory adapter.
|
|
11
11
|
require 'guillotine'
|
12
12
|
module MyApp
|
13
13
|
class App < Guillotine::App
|
14
|
-
|
14
|
+
adapter = Guillotine::Adapters::MemoryAdapter.new
|
15
|
+
set :service => Guillotine::Service.new(adapter)
|
15
16
|
|
16
17
|
get '/' do
|
17
18
|
redirect 'https://homepage.com'
|
@@ -40,14 +41,49 @@ You can specify your own code too:
|
|
40
41
|
|
41
42
|
## Sequel
|
42
43
|
|
43
|
-
The memory adapter sucks though. You probably want to use a DB.
|
44
|
+
The memory adapter sucks though. You probably want to use a DB. Check
|
45
|
+
out the [Sequel gem](http://sequel.rubyforge.org/) for more examples.
|
46
|
+
It'll support SQLite, MySQL, PostgreSQL, and a bunch of other databases.
|
44
47
|
|
45
48
|
```ruby
|
46
49
|
require 'guillotine'
|
47
50
|
require 'sequel'
|
48
51
|
module MyApp
|
49
52
|
class App < Guillotine::App
|
50
|
-
|
53
|
+
db = Sequel.sqlite
|
54
|
+
adapter = Guillotine::Adapters::SequelAdapter.new(db)
|
55
|
+
set :service => Guillotine::Service.new(adapter)
|
56
|
+
end
|
57
|
+
end
|
58
|
+
```
|
59
|
+
|
60
|
+
You'll need to initialize the DB schema with something like this
|
61
|
+
(depending on which DB you use):
|
62
|
+
|
63
|
+
```
|
64
|
+
CREATE TABLE IF NOT EXISTS `urls` (
|
65
|
+
`url` varchar(255) DEFAULT NULL,
|
66
|
+
`code` varchar(255) DEFAULT NULL,
|
67
|
+
UNIQUE KEY `url` (`url`),
|
68
|
+
UNIQUE KEY `code` (`code`)
|
69
|
+
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
|
70
|
+
```
|
71
|
+
|
72
|
+
## Redis
|
73
|
+
|
74
|
+
Redis works well, too. The sample below is [adapted](https://github.com/mrtazz/katana/blob/master/app.rb) from [Katana](https://github.com/mrtazz/katana), a hosted wrapper around Guillotine designed for Heroku.
|
75
|
+
|
76
|
+
```ruby
|
77
|
+
require 'guillotine'
|
78
|
+
require 'redis'
|
79
|
+
|
80
|
+
module MyApp
|
81
|
+
class App < Guillotine::App
|
82
|
+
# use redis adapter with redistogo on Heroku
|
83
|
+
uri = URI.parse(ENV["REDISTOGO_URL"])
|
84
|
+
redis = Redis.new(:host => uri.host, :port => uri.port, :password => uri.password)
|
85
|
+
adapter = Guillotine::Adapters::RedisAdapter.new(redis)
|
86
|
+
set :service => Guillotine::Service.new(adapter)
|
51
87
|
end
|
52
88
|
end
|
53
89
|
```
|
@@ -64,7 +100,8 @@ module MyApp
|
|
64
100
|
class App < Guillotine::App
|
65
101
|
client = Riak::Client.new :protocol => 'pbc', :pb_port => 8087
|
66
102
|
bucket = client['guillotine']
|
67
|
-
|
103
|
+
adapter = Guillotine::Adapters::RiakAdapter.new(bucket)
|
104
|
+
set :service => Guillotine::Service.new(adapter)
|
68
105
|
end
|
69
106
|
end
|
70
107
|
```
|
@@ -77,11 +114,18 @@ You can restrict what domains that Guillotine will shorten.
|
|
77
114
|
require 'guillotine'
|
78
115
|
module MyApp
|
79
116
|
class App < Guillotine::App
|
117
|
+
adapter = Guillotine::Adapters::MemoryAdapter.new
|
80
118
|
# only this domain
|
81
|
-
set :
|
119
|
+
set :service => Guillotine::Service.new(adapter,
|
120
|
+
'github.com')
|
82
121
|
|
83
122
|
# or, any *.github.com domain
|
84
|
-
set :
|
123
|
+
set :service => Guillotine::Service.new(adapter,
|
124
|
+
/(^|\.)github\.com$/)
|
125
|
+
|
126
|
+
# or set a simple wildcard
|
127
|
+
set :service => Guillotine::Servicew.new(adapter,
|
128
|
+
'*.github.com')
|
85
129
|
end
|
86
130
|
end
|
87
131
|
```
|
data/guillotine.gemspec
CHANGED
@@ -13,8 +13,8 @@ Gem::Specification.new do |s|
|
|
13
13
|
## If your rubyforge_project name is different, then edit it and comment out
|
14
14
|
## the sub! line in the Rakefile
|
15
15
|
s.name = 'guillotine'
|
16
|
-
s.version = '1.
|
17
|
-
s.date = '
|
16
|
+
s.version = '1.2.0'
|
17
|
+
s.date = '2012-06-04'
|
18
18
|
s.rubyforge_project = 'guillotine'
|
19
19
|
|
20
20
|
## Make sure your summary is short. The description may be as long
|
@@ -47,6 +47,7 @@ Gem::Specification.new do |s|
|
|
47
47
|
## THE MANIFEST COMMENTS, they are used as delimiters by the task.
|
48
48
|
# = MANIFEST =
|
49
49
|
s.files = %w[
|
50
|
+
CHANGELOG.md
|
50
51
|
Gemfile
|
51
52
|
LICENSE
|
52
53
|
README.md
|
@@ -61,11 +62,13 @@ Gem::Specification.new do |s|
|
|
61
62
|
lib/guillotine/adapters/riak_adapter.rb
|
62
63
|
lib/guillotine/adapters/sequel_adapter.rb
|
63
64
|
lib/guillotine/app.rb
|
65
|
+
lib/guillotine/host_checkers.rb
|
64
66
|
lib/guillotine/service.rb
|
65
67
|
script/cibuild
|
66
68
|
test/active_record_adapter_test.rb
|
67
69
|
test/app_test.rb
|
68
70
|
test/helper.rb
|
71
|
+
test/host_checker_test.rb
|
69
72
|
test/memory_adapter_test.rb
|
70
73
|
test/mongo_adapter_test.rb
|
71
74
|
test/redis_adapter_test.rb
|
data/lib/guillotine.rb
CHANGED
@@ -3,13 +3,12 @@ require 'digest/md5'
|
|
3
3
|
require 'addressable/uri'
|
4
4
|
|
5
5
|
module Guillotine
|
6
|
-
VERSION = "1.
|
6
|
+
VERSION = "1.2.0"
|
7
7
|
|
8
|
-
|
9
|
-
|
10
|
-
autoload :App, "#{dir}/app"
|
8
|
+
class Error < StandardError
|
9
|
+
end
|
11
10
|
|
12
|
-
class DuplicateCodeError <
|
11
|
+
class DuplicateCodeError < Error
|
13
12
|
attr_reader :existing_url, :new_url, :code
|
14
13
|
|
15
14
|
def initialize(existing_url, new_url, code)
|
@@ -20,43 +19,59 @@ module Guillotine
|
|
20
19
|
end
|
21
20
|
end
|
22
21
|
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
#
|
33
|
-
#
|
34
|
-
#
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
# 3) Pack it into a bitstring as a big-endian int
|
41
|
-
# 4) base64-encode the bitstring, remove the trailing junk
|
42
|
-
#
|
43
|
-
# url - String URL to shorten.
|
44
|
-
#
|
45
|
-
# Returns a unique String code for the URL.
|
46
|
-
def shorten(url)
|
47
|
-
Base64.urlsafe_encode64([Digest::MD5.hexdigest(url).to_i(16)].pack("N")).sub(/==\n?$/, '')
|
48
|
-
end
|
22
|
+
# Adapters handle the storage and retrieval of URLs in the system. You can
|
23
|
+
# use whatever you want, as long as it implements the #add and #find
|
24
|
+
# methods. See MemoryAdapter for a simple solution.
|
25
|
+
class Adapter
|
26
|
+
# Public: Shortens a given URL to a short code.
|
27
|
+
#
|
28
|
+
# 1) MD5 hash the URL to the hexdigest
|
29
|
+
# 2) Convert it to a Bignum
|
30
|
+
# 3) Pack it into a bitstring as a big-endian int
|
31
|
+
# 4) base64-encode the bitstring, remove the trailing junk
|
32
|
+
#
|
33
|
+
# url - String URL to shorten.
|
34
|
+
#
|
35
|
+
# Returns a unique String code for the URL.
|
36
|
+
def shorten(url)
|
37
|
+
Base64.urlsafe_encode64([Digest::MD5.hexdigest(url).to_i(16)].pack("N")).sub(/==\n?$/, '')
|
38
|
+
end
|
49
39
|
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
40
|
+
# Parses and sanitizes a URL.
|
41
|
+
#
|
42
|
+
# url - A String URL.
|
43
|
+
#
|
44
|
+
# Returns an Addressable::URI.
|
45
|
+
def parse_url(url)
|
46
|
+
url.gsub! /\s/, ''
|
47
|
+
url.gsub! /(\#|\?).*/, ''
|
48
|
+
Addressable::URI.parse url
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
dir = File.expand_path '../guillotine/adapters', __FILE__
|
53
|
+
autoload :MemoryAdapter, dir + "/memory_adapter"
|
54
|
+
autoload :SequelAdapter, dir + "/sequel_adapter"
|
55
|
+
autoload :RiakAdapter, dir + "/riak_adapter"
|
56
|
+
autoload :ActiveRecordAdapter, dir + "/active_record_adapter"
|
57
|
+
autoload :RedisAdapter, dir + "/redis_adapter"
|
58
|
+
autoload :MongoAdapter, dir + "/mongo_adapter"
|
59
|
+
|
60
|
+
dir = File.expand_path '../guillotine', __FILE__
|
61
|
+
autoload :App, "#{dir}/app"
|
62
|
+
|
63
|
+
require "#{dir}/host_checkers"
|
64
|
+
require "#{dir}/service"
|
65
|
+
|
66
|
+
module Adapters
|
67
|
+
@@warned = false
|
68
|
+
def self.const_missing(*args)
|
69
|
+
unless @@warned
|
70
|
+
puts "Guillotine::Adapters has been deprecated until v2."
|
71
|
+
@@warned = true
|
59
72
|
end
|
73
|
+
puts "Change Guillotine::Adapters::#{args.first} => Guillotine::#{args.first}"
|
74
|
+
::Guillotine.const_get(args.first)
|
60
75
|
end
|
61
76
|
end
|
62
77
|
end
|
@@ -1,79 +1,77 @@
|
|
1
1
|
require 'active_record'
|
2
2
|
|
3
3
|
module Guillotine
|
4
|
-
|
5
|
-
class
|
6
|
-
class Url < ActiveRecord::Base; end
|
4
|
+
class ActiveRecordAdapter < Adapter
|
5
|
+
class Url < ActiveRecord::Base; end
|
7
6
|
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
# Public: Stores the shortened version of a URL.
|
13
|
-
#
|
14
|
-
# url - The String URL to shorten and store.
|
15
|
-
# code - Optional String code for the URL.
|
16
|
-
#
|
17
|
-
# Returns the unique String code for the URL. If the URL is added
|
18
|
-
# multiple times, this should return the same code.
|
19
|
-
def add(url, code = nil)
|
20
|
-
if row = Url.select(:code).where(:url => url).first
|
21
|
-
row[:code]
|
22
|
-
else
|
23
|
-
code ||= shorten url
|
24
|
-
begin
|
25
|
-
Url.create :url => url, :code => code
|
26
|
-
rescue ActiveRecord::RecordNotUnique, ActiveRecord::StatementInvalid
|
27
|
-
row = Url.select(:url).where(:code => code).first
|
28
|
-
existing_url = row && row[:url]
|
29
|
-
raise DuplicateCodeError.new(existing_url, url, code)
|
30
|
-
end
|
31
|
-
code
|
32
|
-
end
|
33
|
-
end
|
7
|
+
def initialize(config)
|
8
|
+
Url.establish_connection config
|
9
|
+
end
|
34
10
|
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
11
|
+
# Public: Stores the shortened version of a URL.
|
12
|
+
#
|
13
|
+
# url - The String URL to shorten and store.
|
14
|
+
# code - Optional String code for the URL.
|
15
|
+
#
|
16
|
+
# Returns the unique String code for the URL. If the URL is added
|
17
|
+
# multiple times, this should return the same code.
|
18
|
+
def add(url, code = nil)
|
19
|
+
if row = Url.select(:code).where(:url => url).first
|
20
|
+
row[:code]
|
21
|
+
else
|
22
|
+
code ||= shorten url
|
23
|
+
begin
|
24
|
+
Url.create :url => url, :code => code
|
25
|
+
rescue ActiveRecord::RecordNotUnique, ActiveRecord::StatementInvalid
|
26
|
+
row = Url.select(:url).where(:code => code).first
|
27
|
+
existing_url = row && row[:url]
|
28
|
+
raise DuplicateCodeError.new(existing_url, url, code)
|
29
|
+
end
|
30
|
+
code
|
42
31
|
end
|
32
|
+
end
|
43
33
|
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
34
|
+
# Public: Retrieves a URL from the code.
|
35
|
+
#
|
36
|
+
# code - The String code to lookup the URL.
|
37
|
+
#
|
38
|
+
# Returns the String URL.
|
39
|
+
def find(code)
|
40
|
+
select :url, :code => code
|
41
|
+
end
|
52
42
|
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
43
|
+
# Public: Retrieves the code for a given URL.
|
44
|
+
#
|
45
|
+
# url - The String URL to lookup.
|
46
|
+
#
|
47
|
+
# Returns the String code, or nil if none is found.
|
48
|
+
def code_for(url)
|
49
|
+
select :code, :url => url
|
50
|
+
end
|
61
51
|
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
52
|
+
# Public: Removes the assigned short code for a URL.
|
53
|
+
#
|
54
|
+
# url - The String URL to remove.
|
55
|
+
#
|
56
|
+
# Returns nothing.
|
57
|
+
def clear(url)
|
58
|
+
Url.where(:url => url).delete_all
|
59
|
+
end
|
68
60
|
|
69
|
-
|
70
|
-
|
61
|
+
def setup
|
62
|
+
conn = Url.connection
|
63
|
+
conn.create_table :urls do |t|
|
64
|
+
t.string :url
|
65
|
+
t.string :code
|
71
66
|
end
|
72
67
|
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
68
|
+
conn.add_index :urls, :url, :unique => true
|
69
|
+
conn.add_index :urls, :code, :unique => true
|
70
|
+
end
|
71
|
+
|
72
|
+
def select(field, query)
|
73
|
+
if row = Url.select(field).where(query).first
|
74
|
+
row[field]
|
77
75
|
end
|
78
76
|
end
|
79
77
|
end
|
@@ -1,62 +1,64 @@
|
|
1
1
|
module Guillotine
|
2
|
-
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
@urls = {}
|
9
|
-
end
|
2
|
+
# Stores shortened URLs in memory. Totally scales.
|
3
|
+
class MemoryAdapter < Adapter
|
4
|
+
attr_reader :hash, :urls
|
5
|
+
def initialize
|
6
|
+
reset
|
7
|
+
end
|
10
8
|
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
end
|
26
|
-
@hash[code] = url
|
27
|
-
@urls[url] = code
|
28
|
-
code
|
9
|
+
# Public: Stores the shortened version of a URL.
|
10
|
+
#
|
11
|
+
# url - The String URL to shorten and store.
|
12
|
+
# code - Optional String code for the URL.
|
13
|
+
#
|
14
|
+
# Returns the unique String code for the URL. If the URL is added
|
15
|
+
# multiple times, this should return the same code.
|
16
|
+
def add(url, code = nil)
|
17
|
+
if existing_code = @urls[url]
|
18
|
+
existing_code
|
19
|
+
else
|
20
|
+
code ||= shorten(url)
|
21
|
+
if existing_url = @hash[code]
|
22
|
+
raise DuplicateCodeError.new(existing_url, url, code) if url != existing_url
|
29
23
|
end
|
24
|
+
@hash[code] = url
|
25
|
+
@urls[url] = code
|
26
|
+
code
|
30
27
|
end
|
28
|
+
end
|
31
29
|
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
30
|
+
# Public: Retrieves a URL from the code.
|
31
|
+
#
|
32
|
+
# code - The String code to lookup the URL.
|
33
|
+
#
|
34
|
+
# Returns the String URL, or nil if none is found.
|
35
|
+
def find(code)
|
36
|
+
@hash[code]
|
37
|
+
end
|
40
38
|
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
39
|
+
# Public: Retrieves the code for a given URL.
|
40
|
+
#
|
41
|
+
# url - The String URL to lookup.
|
42
|
+
#
|
43
|
+
# Returns the String code, or nil if none is found.
|
44
|
+
def code_for(url)
|
45
|
+
@urls[url]
|
46
|
+
end
|
49
47
|
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
end
|
48
|
+
# Public: Removes the assigned short code for a URL.
|
49
|
+
#
|
50
|
+
# url - The String URL to remove.
|
51
|
+
#
|
52
|
+
# Returns nothing.
|
53
|
+
def clear(url)
|
54
|
+
if code = @urls.delete(url)
|
55
|
+
@hash.delete code
|
59
56
|
end
|
60
57
|
end
|
58
|
+
|
59
|
+
def reset
|
60
|
+
@hash = {}
|
61
|
+
@urls = {}
|
62
|
+
end
|
61
63
|
end
|
62
64
|
end
|