guillotine 1.1.0 → 1.2.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/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
|