guillotine 1.2.1 → 1.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/CHANGELOG.md +17 -0
- data/README.md +39 -4
- data/config/cassandra_config.json +10 -0
- data/guillotine.gemspec +6 -2
- data/lib/guillotine.rb +49 -7
- data/lib/guillotine/adapters/active_record_adapter.rb +6 -4
- data/lib/guillotine/adapters/cassandra_adapter.rb +67 -0
- data/lib/guillotine/adapters/memory_adapter.rb +7 -4
- data/lib/guillotine/adapters/mongo_adapter.rb +8 -6
- data/lib/guillotine/adapters/redis_adapter.rb +6 -4
- data/lib/guillotine/adapters/riak_adapter.rb +6 -5
- data/lib/guillotine/adapters/sequel_adapter.rb +5 -4
- data/lib/guillotine/app.rb +7 -0
- data/lib/guillotine/service.rb +56 -9
- data/test/app_test.rb +83 -11
- data/test/cassandra_adapter_test.rb +67 -0
- data/test/helper.rb +3 -0
- data/test/options_test.rb +59 -0
- data/test/service_test.rb +30 -1
- metadata +76 -48
data/CHANGELOG.md
CHANGED
@@ -1,5 +1,22 @@
|
|
1
1
|
# Changelog
|
2
2
|
|
3
|
+
## v1.3.0
|
4
|
+
|
5
|
+
* Add Cassandra Adapter [rajeshucsb]
|
6
|
+
* Add option to stop stripping URL queries or Anchors. [mrtazz]
|
7
|
+
|
8
|
+
class MyApp < Guillotine::App
|
9
|
+
db = Sequel.sqlite
|
10
|
+
adapter = Guillotine::Adapters::SequelAdapter.new(db)
|
11
|
+
|
12
|
+
# OLD required host configuration, deprecated
|
13
|
+
set :service => Guillotine::Service.new(adapter, 'github.com')
|
14
|
+
|
15
|
+
# NEW required host configuration.
|
16
|
+
set :service => Guillotine::Service.new(adapter,
|
17
|
+
:required_host => 'github.com', :strip_query => false)
|
18
|
+
end
|
19
|
+
|
3
20
|
## v1.2.1
|
4
21
|
|
5
22
|
* Fix WildcardHostChecker error responses.
|
data/README.md
CHANGED
@@ -50,7 +50,7 @@ require 'guillotine'
|
|
50
50
|
require 'sequel'
|
51
51
|
module MyApp
|
52
52
|
class App < Guillotine::App
|
53
|
-
db = Sequel.sqlite
|
53
|
+
db = Sequel.sqlite
|
54
54
|
adapter = Guillotine::Adapters::SequelAdapter.new(db)
|
55
55
|
set :service => Guillotine::Service.new(adapter)
|
56
56
|
end
|
@@ -106,6 +106,41 @@ module MyApp
|
|
106
106
|
end
|
107
107
|
```
|
108
108
|
|
109
|
+
## Cassandra
|
110
|
+
|
111
|
+
you can use Cassandra!
|
112
|
+
|
113
|
+
```ruby
|
114
|
+
require 'guillotine'
|
115
|
+
require 'cassandra'
|
116
|
+
|
117
|
+
module MyApp
|
118
|
+
class App < Guillotine::App
|
119
|
+
cassandra = Cassandra.new('url_shortener', '127.0.0.1:9160')
|
120
|
+
adapter = Guillotine::Adapters::CassandraAdapter.new(cassandra)
|
121
|
+
|
122
|
+
set :service => Guillotine::Service.new(adapter)
|
123
|
+
end
|
124
|
+
end
|
125
|
+
```
|
126
|
+
|
127
|
+
You need to create keyspace and column families as below
|
128
|
+
|
129
|
+
```sql
|
130
|
+
CREATE KEYSPACE url_shortener;
|
131
|
+
USE url_shortener;
|
132
|
+
|
133
|
+
CREATE COLUMN FAMILY urls
|
134
|
+
WITH comparator = UTF8Type
|
135
|
+
AND key_validation_class=UTF8Type
|
136
|
+
AND column_metadata = [{column_name: code, validation_class: UTF8Type}];
|
137
|
+
|
138
|
+
CREATE COLUMN FAMILY codes
|
139
|
+
WITH comparator = UTF8Type
|
140
|
+
AND key_validation_class=UTF8Type
|
141
|
+
AND column_metadata = [{column_name: url, validation_class: UTF8Type}];
|
142
|
+
```
|
143
|
+
|
109
144
|
## Domain Restriction
|
110
145
|
|
111
146
|
You can restrict what domains that Guillotine will shorten.
|
@@ -117,15 +152,15 @@ module MyApp
|
|
117
152
|
adapter = Guillotine::Adapters::MemoryAdapter.new
|
118
153
|
# only this domain
|
119
154
|
set :service => Guillotine::Service.new(adapter,
|
120
|
-
'github.com')
|
155
|
+
:required_host => 'github.com')
|
121
156
|
|
122
157
|
# or, any *.github.com domain
|
123
158
|
set :service => Guillotine::Service.new(adapter,
|
124
|
-
/(^|\.)github\.com$/)
|
159
|
+
:required_host => /(^|\.)github\.com$/)
|
125
160
|
|
126
161
|
# or set a simple wildcard
|
127
162
|
set :service => Guillotine::Servicew.new(adapter,
|
128
|
-
'*.github.com')
|
163
|
+
:required_host => '*.github.com')
|
129
164
|
end
|
130
165
|
end
|
131
166
|
```
|
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 = '2012-
|
16
|
+
s.version = '1.3.0'
|
17
|
+
s.date = '2012-08-19'
|
18
18
|
s.rubyforge_project = 'guillotine'
|
19
19
|
|
20
20
|
## Make sure your summary is short. The description may be as long
|
@@ -52,10 +52,12 @@ Gem::Specification.new do |s|
|
|
52
52
|
LICENSE
|
53
53
|
README.md
|
54
54
|
Rakefile
|
55
|
+
config/cassandra_config.json
|
55
56
|
config/haproxy.riak.cfg
|
56
57
|
guillotine.gemspec
|
57
58
|
lib/guillotine.rb
|
58
59
|
lib/guillotine/adapters/active_record_adapter.rb
|
60
|
+
lib/guillotine/adapters/cassandra_adapter.rb
|
59
61
|
lib/guillotine/adapters/memory_adapter.rb
|
60
62
|
lib/guillotine/adapters/mongo_adapter.rb
|
61
63
|
lib/guillotine/adapters/redis_adapter.rb
|
@@ -67,10 +69,12 @@ Gem::Specification.new do |s|
|
|
67
69
|
script/cibuild
|
68
70
|
test/active_record_adapter_test.rb
|
69
71
|
test/app_test.rb
|
72
|
+
test/cassandra_adapter_test.rb
|
70
73
|
test/helper.rb
|
71
74
|
test/host_checker_test.rb
|
72
75
|
test/memory_adapter_test.rb
|
73
76
|
test/mongo_adapter_test.rb
|
77
|
+
test/options_test.rb
|
74
78
|
test/redis_adapter_test.rb
|
75
79
|
test/riak_adapter_test.rb
|
76
80
|
test/sequel_adapter_test.rb
|
data/lib/guillotine.rb
CHANGED
@@ -3,7 +3,7 @@ require 'digest/md5'
|
|
3
3
|
require 'addressable/uri'
|
4
4
|
|
5
5
|
module Guillotine
|
6
|
-
VERSION = "1.
|
6
|
+
VERSION = "1.3.0"
|
7
7
|
|
8
8
|
class Error < StandardError
|
9
9
|
end
|
@@ -23,7 +23,7 @@ module Guillotine
|
|
23
23
|
# use whatever you want, as long as it implements the #add and #find
|
24
24
|
# methods. See MemoryAdapter for a simple solution.
|
25
25
|
class Adapter
|
26
|
-
#
|
26
|
+
# Internal: Shortens a given URL to a short code.
|
27
27
|
#
|
28
28
|
# 1) MD5 hash the URL to the hexdigest
|
29
29
|
# 2) Convert it to a Bignum
|
@@ -37,16 +37,57 @@ module Guillotine
|
|
37
37
|
Base64.urlsafe_encode64([Digest::MD5.hexdigest(url).to_i(16)].pack("N")).sub(/==\n?$/, '')
|
38
38
|
end
|
39
39
|
|
40
|
+
# Internal: Shortens a URL with a specific character set at a certain
|
41
|
+
# length.
|
42
|
+
#
|
43
|
+
# url - String URL to shorten.
|
44
|
+
# length - Optional Integer maximum length of the short code desired.
|
45
|
+
# charset - Optional Array of String characters which will be present in
|
46
|
+
# short code. eg. ['a', 'b', 'c', 'd', 'e', 'f']
|
47
|
+
#
|
48
|
+
# Returns an encoded String code for the URL.
|
49
|
+
def shorten_fixed_charset(url, length, char_set)
|
50
|
+
number = (Digest::MD5.hexdigest(url).to_i(16) % (char_set.size**length))
|
51
|
+
|
52
|
+
code = ""
|
53
|
+
|
54
|
+
while (number > 0)
|
55
|
+
code = code + char_set[number % char_set.size]
|
56
|
+
number /= char_set.size
|
57
|
+
end
|
58
|
+
|
59
|
+
code
|
60
|
+
end
|
61
|
+
|
40
62
|
# Parses and sanitizes a URL.
|
41
63
|
#
|
42
|
-
# url
|
64
|
+
# url - A String URL.
|
65
|
+
# options - A Guillotine::Service::Options object.
|
43
66
|
#
|
44
67
|
# Returns an Addressable::URI.
|
45
|
-
def parse_url(url)
|
46
|
-
url.gsub!
|
47
|
-
url.gsub!
|
48
|
-
|
68
|
+
def parse_url(url, options)
|
69
|
+
url.gsub!(/\s/, '')
|
70
|
+
url.gsub!(/\?.*/, '') if options.strip_query?
|
71
|
+
url.gsub!(/\#.*/, '') if options.strip_anchor?
|
72
|
+
Addressable::URI.parse(url)
|
49
73
|
end
|
74
|
+
|
75
|
+
# Internal: Shortens a URL with the given options.
|
76
|
+
#
|
77
|
+
# url - A String URL.
|
78
|
+
# code - Optional String code.
|
79
|
+
# options - Optional Guillotine::Service::Options to specify how the code
|
80
|
+
# is generated.
|
81
|
+
#
|
82
|
+
# returns a String code.
|
83
|
+
def get_code(url, code = nil, options = nil)
|
84
|
+
code ||= if options && options.with_charset?
|
85
|
+
shorten_fixed_charset(url, options.length, options.charset)
|
86
|
+
else
|
87
|
+
shorten(url)
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
50
91
|
end
|
51
92
|
|
52
93
|
dir = File.expand_path '../guillotine/adapters', __FILE__
|
@@ -56,6 +97,7 @@ module Guillotine
|
|
56
97
|
autoload :ActiveRecordAdapter, dir + "/active_record_adapter"
|
57
98
|
autoload :RedisAdapter, dir + "/redis_adapter"
|
58
99
|
autoload :MongoAdapter, dir + "/mongo_adapter"
|
100
|
+
autoload :CassandraAdapter, dir + "/cassandra_adapter"
|
59
101
|
|
60
102
|
dir = File.expand_path '../guillotine', __FILE__
|
61
103
|
autoload :App, "#{dir}/app"
|
@@ -10,16 +10,18 @@ module Guillotine
|
|
10
10
|
|
11
11
|
# Public: Stores the shortened version of a URL.
|
12
12
|
#
|
13
|
-
# url
|
14
|
-
# code
|
13
|
+
# url - The String URL to shorten and store.
|
14
|
+
# code - Optional String code for the URL.
|
15
|
+
# options - Optional Guillotine::Service::Options
|
15
16
|
#
|
16
17
|
# Returns the unique String code for the URL. If the URL is added
|
17
18
|
# multiple times, this should return the same code.
|
18
|
-
def add(url, code = nil)
|
19
|
+
def add(url, code = nil, options = nil)
|
19
20
|
if row = Url.select(:code).where(:url => url).first
|
20
21
|
row[:code]
|
21
22
|
else
|
22
|
-
code
|
23
|
+
code = get_code(url, code, options)
|
24
|
+
|
23
25
|
begin
|
24
26
|
Url.create :url => url, :code => code
|
25
27
|
rescue ActiveRecord::RecordNotUnique, ActiveRecord::StatementInvalid
|
@@ -0,0 +1,67 @@
|
|
1
|
+
module Guillotine
|
2
|
+
class CassandraAdapter < Adapter
|
3
|
+
# Public: Initialise the adapter with a Redis instance.
|
4
|
+
#
|
5
|
+
# cassandra - A Cassandra instance to persist urls and codes to.
|
6
|
+
def initialize(cassandra, read_only = false)
|
7
|
+
@cassandra = cassandra
|
8
|
+
@read_only = read_only
|
9
|
+
end
|
10
|
+
|
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
|
+
# options - Optional Guillotine::Service::Options
|
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, options = nil)
|
20
|
+
return if @read_only
|
21
|
+
if existing_code = code_for(url)
|
22
|
+
existing_code
|
23
|
+
else
|
24
|
+
code = get_code(url, code, options)
|
25
|
+
|
26
|
+
if existing_url = find(code)
|
27
|
+
raise DuplicateCodeError.new(existing_url, url, code) if url != existing_url
|
28
|
+
end
|
29
|
+
@cassandra.insert("codes", code, 'url' => url)
|
30
|
+
@cassandra.insert("urls", url, 'code' => code)
|
31
|
+
code
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
# Public: Retrieves a URL from the code.
|
36
|
+
#
|
37
|
+
# code - The String code to lookup the URL.
|
38
|
+
#
|
39
|
+
# Returns the String URL, or nil if none is found.
|
40
|
+
def find(code)
|
41
|
+
obj = @cassandra.get("codes", code)
|
42
|
+
obj.nil? ? nil : obj["url"]
|
43
|
+
end
|
44
|
+
|
45
|
+
# Public: Retrieves the code for a given URL.
|
46
|
+
#
|
47
|
+
# url - The String URL to lookup.
|
48
|
+
#
|
49
|
+
# Returns the String code, or nil if none is found.
|
50
|
+
def code_for(url)
|
51
|
+
obj = @cassandra.get("urls", url)
|
52
|
+
obj.nil? ? nil : obj["code"]
|
53
|
+
end
|
54
|
+
|
55
|
+
# Public: Removes the assigned short code for a URL.
|
56
|
+
#
|
57
|
+
# url - The String URL to remove.
|
58
|
+
#
|
59
|
+
# Returns nothing.
|
60
|
+
def clear(url)
|
61
|
+
if code = code_for(url)
|
62
|
+
@cassandra.remove("urls", url)
|
63
|
+
@cassandra.remove("codes", code)
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
@@ -8,16 +8,18 @@ module Guillotine
|
|
8
8
|
|
9
9
|
# Public: Stores the shortened version of a URL.
|
10
10
|
#
|
11
|
-
# url
|
12
|
-
# code
|
11
|
+
# url - The String URL to shorten and store.
|
12
|
+
# code - Optional String code for the URL.
|
13
|
+
# options - Optional Guillotine::Service::Options
|
13
14
|
#
|
14
15
|
# Returns the unique String code for the URL. If the URL is added
|
15
16
|
# multiple times, this should return the same code.
|
16
|
-
def add(url, code = nil)
|
17
|
+
def add(url, code = nil, options = nil)
|
17
18
|
if existing_code = @urls[url]
|
18
19
|
existing_code
|
19
20
|
else
|
20
|
-
code
|
21
|
+
code = get_code(url, code, options)
|
22
|
+
|
21
23
|
if existing_url = @hash[code]
|
22
24
|
raise DuplicateCodeError.new(existing_url, url, code) if url != existing_url
|
23
25
|
end
|
@@ -62,3 +64,4 @@ module Guillotine
|
|
62
64
|
end
|
63
65
|
end
|
64
66
|
end
|
67
|
+
|
@@ -15,13 +15,14 @@ module Guillotine
|
|
15
15
|
|
16
16
|
# Public: Stores the shortened version of a URL.
|
17
17
|
#
|
18
|
-
# url
|
19
|
-
# code
|
18
|
+
# url - The String URL to shorten and store.
|
19
|
+
# code - Optional String code for the URL.
|
20
|
+
# options - Optional Guillotine::Service::Options
|
20
21
|
#
|
21
22
|
# Returns the unique String code for the URL. If the URL is added
|
22
23
|
# multiple times, this should return the same code.
|
23
|
-
def add(url, code = nil)
|
24
|
-
code_for(url) || insert(url, code
|
24
|
+
def add(url, code = nil, options = nil)
|
25
|
+
code_for(url) || insert(url, get_code(url, code, options))
|
25
26
|
end
|
26
27
|
|
27
28
|
|
@@ -31,7 +32,7 @@ module Guillotine
|
|
31
32
|
#
|
32
33
|
# Returns the String URL, or nil if none is found.
|
33
34
|
def find(code)
|
34
|
-
select
|
35
|
+
select(:url, :_id => code)
|
35
36
|
end
|
36
37
|
|
37
38
|
# Public: Retrieves the code for a given URL.
|
@@ -40,7 +41,7 @@ module Guillotine
|
|
40
41
|
#
|
41
42
|
# Returns the String code, or nil if none is found.
|
42
43
|
def code_for(url)
|
43
|
-
select
|
44
|
+
select(:code, :url => url)
|
44
45
|
end
|
45
46
|
|
46
47
|
# Public: Removes the assigned short code for a URL.
|
@@ -66,3 +67,4 @@ module Guillotine
|
|
66
67
|
end
|
67
68
|
end
|
68
69
|
end
|
70
|
+
|
@@ -9,16 +9,18 @@ module Guillotine
|
|
9
9
|
|
10
10
|
# Public: Stores the shortened version of a URL.
|
11
11
|
#
|
12
|
-
# url
|
13
|
-
# code
|
12
|
+
# url - The String URL to shorten and store.
|
13
|
+
# code - Optional String code for the URL.
|
14
|
+
# options - Optional Guillotine::Service::Options
|
14
15
|
#
|
15
16
|
# Returns the unique String code for the URL. If the URL is added
|
16
17
|
# multiple times, this should return the same code.
|
17
|
-
def add(url, code = nil)
|
18
|
+
def add(url, code = nil, options = nil)
|
18
19
|
if existing_code = @redis.get(url_key(url))
|
19
20
|
existing_code
|
20
21
|
else
|
21
|
-
code
|
22
|
+
code = get_code(url, code, options)
|
23
|
+
|
22
24
|
if existing_url = @redis.get(code_key(code))
|
23
25
|
raise DuplicateCodeError.new(existing_url, url, code) if url != existing_url
|
24
26
|
end
|
@@ -17,13 +17,14 @@ module Guillotine
|
|
17
17
|
end
|
18
18
|
|
19
19
|
# Public: Stores the shortened version of a URL.
|
20
|
-
#
|
21
|
-
# url
|
22
|
-
# code
|
20
|
+
#
|
21
|
+
# url - The String URL to shorten and store.
|
22
|
+
# code - Optional String code for the URL.
|
23
|
+
# options - Optional Guillotine::Service::Options
|
23
24
|
#
|
24
25
|
# Returns the unique String code for the URL. If the URL is added
|
25
26
|
# multiple times, this should return the same code.
|
26
|
-
def add(url, code = nil)
|
27
|
+
def add(url, code = nil, options = nil)
|
27
28
|
sha = url_key url
|
28
29
|
url_obj = @url_bucket.get_or_new sha, :r => 1
|
29
30
|
if url_obj.raw_data
|
@@ -31,7 +32,7 @@ module Guillotine
|
|
31
32
|
code = url_obj.data
|
32
33
|
end
|
33
34
|
|
34
|
-
code
|
35
|
+
code = get_code(url, code, options)
|
35
36
|
code_obj = @code_bucket.get_or_new code
|
36
37
|
code_obj.content_type = url_obj.content_type = PLAIN
|
37
38
|
|
@@ -7,16 +7,17 @@ module Guillotine
|
|
7
7
|
|
8
8
|
# Public: Stores the shortened version of a URL.
|
9
9
|
#
|
10
|
-
# url
|
11
|
-
# code
|
10
|
+
# url - The String URL to shorten and store.
|
11
|
+
# code - Optional String code for the URL.
|
12
|
+
# options - Optional Guillotine::Service::Options
|
12
13
|
#
|
13
14
|
# Returns the unique String code for the URL. If the URL is added
|
14
15
|
# multiple times, this should return the same code.
|
15
|
-
def add(url, code = nil)
|
16
|
+
def add(url, code = nil, options = nil)
|
16
17
|
if existing = code_for(url)
|
17
18
|
existing
|
18
19
|
else
|
19
|
-
code
|
20
|
+
code = get_code(url, code, options)
|
20
21
|
begin
|
21
22
|
@table << {:url => url, :code => code}
|
22
23
|
rescue Sequel::DatabaseError
|
data/lib/guillotine/app.rb
CHANGED
@@ -6,6 +6,13 @@ module Guillotine
|
|
6
6
|
class App < Sinatra::Base
|
7
7
|
set :service, nil
|
8
8
|
|
9
|
+
get "/" do
|
10
|
+
if params[:code].nil?
|
11
|
+
default_url = settings.service.default_url
|
12
|
+
redirect default_url if !default_url.nil?
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
9
16
|
get "/:code" do
|
10
17
|
escaped = Addressable::URI.escape(params[:code])
|
11
18
|
status, head, body = settings.service.get(escaped)
|
data/lib/guillotine/service.rb
CHANGED
@@ -3,6 +3,46 @@ module Guillotine
|
|
3
3
|
# Deprecated until v2
|
4
4
|
NullChecker = Guillotine::HostChecker
|
5
5
|
|
6
|
+
# length - Optional Integer maximum length of the short code desired.
|
7
|
+
# charset - Optional Array of String characters which will be present in
|
8
|
+
# short code. eg. ['a', 'b', 'c', 'd', 'e', 'f']
|
9
|
+
class Options < Struct.new(:required_host, :strip_query, :strip_anchor,
|
10
|
+
:length, :charset, :default_url)
|
11
|
+
def self.from(value)
|
12
|
+
case value
|
13
|
+
when nil, "" then new
|
14
|
+
when String, Regexp then new(value)
|
15
|
+
when Hash then
|
16
|
+
opt = new
|
17
|
+
value.each do |key, value|
|
18
|
+
opt[key] = value
|
19
|
+
end
|
20
|
+
opt
|
21
|
+
when self then value
|
22
|
+
else
|
23
|
+
raise ArgumentError, "Unable to convert to Options: #{value.inspect}"
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
def strip_query?
|
28
|
+
strip_query != false
|
29
|
+
end
|
30
|
+
|
31
|
+
def strip_anchor?
|
32
|
+
strip_anchor != false
|
33
|
+
end
|
34
|
+
|
35
|
+
def with_charset?
|
36
|
+
!(length.nil? || charset.nil?)
|
37
|
+
end
|
38
|
+
|
39
|
+
def host_checker
|
40
|
+
@host_checker ||= HostChecker.matching(required_host)
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
attr_reader :db, :options
|
45
|
+
|
6
46
|
# This is the public API to the Guillotine service. Wire this up to Sinatra
|
7
47
|
# or whatever. Every public method should return a compatible Rack Response:
|
8
48
|
# [Integer Status, Hash headers, String body].
|
@@ -11,9 +51,9 @@ module Guillotine
|
|
11
51
|
# required_host - Either a String or Regex limiting which domains the
|
12
52
|
# shortened URLs can come from.
|
13
53
|
#
|
14
|
-
def initialize(db,
|
54
|
+
def initialize(db, value = nil)
|
15
55
|
@db = db
|
16
|
-
@
|
56
|
+
@options = Options.from(value)
|
17
57
|
end
|
18
58
|
|
19
59
|
# Public: Gets the full URL for a shortened code.
|
@@ -24,7 +64,7 @@ module Guillotine
|
|
24
64
|
# or 404 on a miss.
|
25
65
|
def get(code)
|
26
66
|
if url = @db.find(code)
|
27
|
-
[302, {"Location" =>
|
67
|
+
[302, {"Location" => parse_url(url).to_s}]
|
28
68
|
else
|
29
69
|
[404, {}, "No url found for #{code}"]
|
30
70
|
end
|
@@ -44,7 +84,7 @@ module Guillotine
|
|
44
84
|
end
|
45
85
|
|
46
86
|
begin
|
47
|
-
if code = @db.add(url.to_s, code)
|
87
|
+
if code = @db.add(url.to_s, code, @options)
|
48
88
|
[201, {"Location" => code}]
|
49
89
|
else
|
50
90
|
[422, {}, "Unable to shorten #{url}"]
|
@@ -64,7 +104,7 @@ module Guillotine
|
|
64
104
|
if url.scheme !~ /^https?$/
|
65
105
|
[422, {}, "Invalid url: #{url}"]
|
66
106
|
else
|
67
|
-
@
|
107
|
+
@options.host_checker.call url
|
68
108
|
end
|
69
109
|
end
|
70
110
|
|
@@ -77,11 +117,18 @@ module Guillotine
|
|
77
117
|
if str.respond_to?(:scheme)
|
78
118
|
str
|
79
119
|
else
|
80
|
-
str
|
81
|
-
str.gsub! /\s/, ''
|
82
|
-
str.gsub! /(\#|\?).*/, ''
|
83
|
-
Addressable::URI.parse str
|
120
|
+
parse_url(str.to_s)
|
84
121
|
end
|
85
122
|
end
|
123
|
+
|
124
|
+
# Internal
|
125
|
+
def parse_url(url)
|
126
|
+
@db.parse_url(url, @options)
|
127
|
+
end
|
128
|
+
|
129
|
+
# Public
|
130
|
+
def default_url
|
131
|
+
@options.default_url
|
132
|
+
end
|
86
133
|
end
|
87
134
|
end
|
data/test/app_test.rb
CHANGED
@@ -10,7 +10,7 @@ module Guillotine
|
|
10
10
|
|
11
11
|
def test_adding_a_link_returns_code
|
12
12
|
url = 'http://github.com'
|
13
|
-
post '/', :url => url
|
13
|
+
post '/', :url => url
|
14
14
|
assert_equal 201, last_response.status
|
15
15
|
assert code_url = last_response.headers['Location']
|
16
16
|
code = code_url.gsub(/.*\//, '')
|
@@ -20,6 +20,60 @@ module Guillotine
|
|
20
20
|
assert_equal url, last_response.headers['Location']
|
21
21
|
end
|
22
22
|
|
23
|
+
def test_adding_a_link_with_query_params_strips_query
|
24
|
+
query_url = 'http://github.com?a=1'
|
25
|
+
url = 'http://github.com'
|
26
|
+
post '/', :url => query_url
|
27
|
+
assert_equal 201, last_response.status
|
28
|
+
assert code_url = last_response.headers['Location']
|
29
|
+
code = code_url.gsub(/.*\//, '')
|
30
|
+
|
31
|
+
get "/#{code}"
|
32
|
+
assert_equal 302, last_response.status
|
33
|
+
assert_equal url, last_response.headers['Location']
|
34
|
+
end
|
35
|
+
|
36
|
+
def test_adding_a_link_with_query_params_returns_code
|
37
|
+
with_service :strip_query => false do
|
38
|
+
url = 'http://github.com?a=1'
|
39
|
+
post '/', :url => url
|
40
|
+
assert_equal 201, last_response.status
|
41
|
+
assert code_url = last_response.headers['Location']
|
42
|
+
code = code_url.gsub(/.*\//, '')
|
43
|
+
|
44
|
+
get "/#{code}"
|
45
|
+
assert_equal 302, last_response.status
|
46
|
+
assert_equal url, last_response.headers['Location']
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
def test_adding_a_link_with_anchor_strips_anchor
|
51
|
+
query_url = 'http://github.com?a=1#a'
|
52
|
+
url = 'http://github.com'
|
53
|
+
post '/', :url => query_url
|
54
|
+
assert_equal 201, last_response.status
|
55
|
+
assert code_url = last_response.headers['Location']
|
56
|
+
code = code_url.gsub(/.*\//, '')
|
57
|
+
|
58
|
+
get "/#{code}"
|
59
|
+
assert_equal 302, last_response.status
|
60
|
+
assert_equal url, last_response.headers['Location']
|
61
|
+
end
|
62
|
+
|
63
|
+
def test_adding_a_link_with_anchor_params_returns_code
|
64
|
+
with_service :strip_anchor => false do
|
65
|
+
url = 'http://github.com#a'
|
66
|
+
post '/', :url => url
|
67
|
+
assert_equal 201, last_response.status
|
68
|
+
assert code_url = last_response.headers['Location']
|
69
|
+
code = code_url.gsub(/.*\//, '')
|
70
|
+
|
71
|
+
get "/#{code}"
|
72
|
+
assert_equal 302, last_response.status
|
73
|
+
assert_equal url, last_response.headers['Location']
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
23
77
|
def test_adding_duplicate_link_returns_same_code
|
24
78
|
url = 'http://github.com'
|
25
79
|
code = ADAPTER.add url
|
@@ -101,23 +155,41 @@ module Guillotine
|
|
101
155
|
end
|
102
156
|
|
103
157
|
def test_reject_shortened_url_from_other_domain_by_regex
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
158
|
+
with_service /abc\.com$/ do
|
159
|
+
post '/', :url => 'http://github.com'
|
160
|
+
assert_equal 422, last_response.status
|
161
|
+
assert_match /must match \/abc\\.com/, last_response.body
|
108
162
|
|
109
|
-
|
110
|
-
|
163
|
+
post '/', :url => 'http://abc.com/def'
|
164
|
+
assert_equal 201, last_response.status
|
111
165
|
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
166
|
+
post '/', :url => 'http://www.abc.com/def'
|
167
|
+
assert_equal 201, last_response.status
|
168
|
+
end
|
169
|
+
end
|
170
|
+
|
171
|
+
def test_get_without_code_returns_default_url
|
172
|
+
with_service :default_url => 'http://google.com' do
|
173
|
+
get '/'
|
174
|
+
assert_equal "http://google.com", last_response.headers['location']
|
175
|
+
end
|
176
|
+
end
|
177
|
+
|
178
|
+
def test_get_without_code_no_default_url
|
179
|
+
get '/'
|
180
|
+
assert_equal nil, last_response.headers['location']
|
116
181
|
end
|
117
182
|
|
118
183
|
def app
|
119
184
|
App
|
120
185
|
end
|
186
|
+
|
187
|
+
def with_service(options)
|
188
|
+
App.set :service, Service.new(ADAPTER, options)
|
189
|
+
yield
|
190
|
+
ensure
|
191
|
+
App.set :service, SERVICE
|
192
|
+
end
|
121
193
|
end
|
122
194
|
end
|
123
195
|
|
@@ -0,0 +1,67 @@
|
|
1
|
+
require File.expand_path('../helper', __FILE__)
|
2
|
+
|
3
|
+
begin
|
4
|
+
require "rubygems"
|
5
|
+
require "cassandra"
|
6
|
+
require 'cassandra/mock'
|
7
|
+
|
8
|
+
class CassandraAdapterTest < Guillotine::TestCase
|
9
|
+
@test_schema = JSON.parse(File.read(File.join(File.expand_path(File.dirname(__FILE__)), '..','config', 'cassandra_config.json')))
|
10
|
+
@cassandra_mock = Cassandra::Mock.new('url_shortener', @test_schema)
|
11
|
+
@cassandra_mock.clear_keyspace!
|
12
|
+
ADAPTER = Guillotine::CassandraAdapter.new @cassandra_mock
|
13
|
+
|
14
|
+
def setup
|
15
|
+
@db = ADAPTER
|
16
|
+
end
|
17
|
+
|
18
|
+
def test_adding_a_link_returns_code
|
19
|
+
code = @db.add 'abc'
|
20
|
+
assert_equal 'abc', @db.find(code)
|
21
|
+
end
|
22
|
+
|
23
|
+
def test_adding_duplicate_link_returns_same_code
|
24
|
+
code = @db.add 'abc'
|
25
|
+
assert_equal code, @db.add('abc')
|
26
|
+
end
|
27
|
+
|
28
|
+
def test_adds_url_with_custom_code
|
29
|
+
assert_equal 'code', @db.add('def', 'code')
|
30
|
+
assert_equal 'def', @db.find('code')
|
31
|
+
end
|
32
|
+
|
33
|
+
def test_clashing_urls_raises_error
|
34
|
+
code = @db.add 'abc'
|
35
|
+
assert_raise Guillotine::DuplicateCodeError do
|
36
|
+
@db.add 'ghi', code
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
def test_missing_code
|
41
|
+
assert_nil @db.find('missing')
|
42
|
+
end
|
43
|
+
|
44
|
+
def test_gets_code_for_url
|
45
|
+
code = @db.add 'abc'
|
46
|
+
assert_equal code, @db.code_for('abc')
|
47
|
+
end
|
48
|
+
|
49
|
+
def test_clears_code_for_url
|
50
|
+
code = @db.add 'abc'
|
51
|
+
assert_equal 'abc', @db.find(code)
|
52
|
+
|
53
|
+
@db.clear 'abc'
|
54
|
+
|
55
|
+
assert_nil @db.find(code)
|
56
|
+
end
|
57
|
+
|
58
|
+
def test_read_only
|
59
|
+
Guillotine::CassandraAdapter.new @cassandra_mock, true
|
60
|
+
code = @db.add 'abc'
|
61
|
+
assert_equal nil, code
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
rescue LoadError
|
66
|
+
puts "Skipping Cassandra tests: #{$!}"
|
67
|
+
end
|
data/test/helper.rb
CHANGED
@@ -0,0 +1,59 @@
|
|
1
|
+
require File.expand_path('../helper', __FILE__)
|
2
|
+
|
3
|
+
module Guillotine
|
4
|
+
class OptionsTest < TestCase
|
5
|
+
def test_parses_from_options
|
6
|
+
options = Service::Options.new
|
7
|
+
assert_equal options.object_id, Service::Options.from(options).object_id
|
8
|
+
end
|
9
|
+
|
10
|
+
def test_parses_from_string
|
11
|
+
options = Service::Options.from('abc')
|
12
|
+
assert_equal 'abc', options.required_host
|
13
|
+
assert options.strip_query?
|
14
|
+
assert options.strip_anchor?
|
15
|
+
end
|
16
|
+
|
17
|
+
def test_parses_from_regex
|
18
|
+
options = Service::Options.from(/abc/)
|
19
|
+
assert_equal /abc/, options.required_host
|
20
|
+
assert options.strip_query?
|
21
|
+
assert options.strip_anchor?
|
22
|
+
end
|
23
|
+
|
24
|
+
def test_parses_from_hash
|
25
|
+
options = Service::Options.from(:strip_query => true,
|
26
|
+
:strip_anchor => false)
|
27
|
+
assert_nil options.required_host
|
28
|
+
assert options.strip_query?
|
29
|
+
assert !options.strip_anchor?
|
30
|
+
end
|
31
|
+
|
32
|
+
def test_parses_from_bad_hash
|
33
|
+
assert_raises NameError do
|
34
|
+
Service::Options.from :foo => 1
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
def test_parses_from_unknown
|
39
|
+
assert_raises ArgumentError do
|
40
|
+
Service::Options.from 123
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
def test_parses_from_empty_string
|
45
|
+
options = Service::Options.from('')
|
46
|
+
assert_nil options.required_host
|
47
|
+
assert options.strip_query?
|
48
|
+
assert options.strip_anchor?
|
49
|
+
end
|
50
|
+
|
51
|
+
def test_parses_from_nil
|
52
|
+
options = Service::Options.from(nil)
|
53
|
+
assert_nil options.required_host
|
54
|
+
assert options.strip_query?
|
55
|
+
assert options.strip_anchor?
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
data/test/service_test.rb
CHANGED
@@ -9,7 +9,19 @@ module Guillotine
|
|
9
9
|
|
10
10
|
def test_adding_a_link_returns_code
|
11
11
|
url = 'http://github.com'
|
12
|
-
status, head, body = @service.create(url
|
12
|
+
status, head, body = @service.create(url)
|
13
|
+
assert_equal 201, status
|
14
|
+
assert code_url = head['Location']
|
15
|
+
code = code_url.gsub(/.*\//, '')
|
16
|
+
|
17
|
+
status, head, body = @service.get(code)
|
18
|
+
assert_equal 302, status
|
19
|
+
assert_equal url, head['Location']
|
20
|
+
end
|
21
|
+
|
22
|
+
def test_adding_a_link_with_query_param_returns_code
|
23
|
+
url = 'http://github.com?a=1'
|
24
|
+
status, head, body = @service.create(url)
|
13
25
|
assert_equal 201, status
|
14
26
|
assert code_url = head['Location']
|
15
27
|
code = code_url.gsub(/.*\//, '')
|
@@ -110,6 +122,23 @@ module Guillotine
|
|
110
122
|
status, head, body = service.create('http://www.abc.com/def')
|
111
123
|
assert_equal 201, status
|
112
124
|
end
|
125
|
+
|
126
|
+
def test_fixed_charset_code
|
127
|
+
@db = MemoryAdapter.new
|
128
|
+
length = 4
|
129
|
+
char_set = ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z']
|
130
|
+
@service = Service.new @db, :length => length, :charset => char_set
|
131
|
+
|
132
|
+
url = 'http://github.com'
|
133
|
+
status, head, body = @service.create(url)
|
134
|
+
assert_equal 201, status
|
135
|
+
assert code_url = head['Location']
|
136
|
+
|
137
|
+
assert_equal 4, code_url.length
|
138
|
+
code_url.each_char do |c|
|
139
|
+
assert char_set.include?(c)
|
140
|
+
end
|
141
|
+
end
|
113
142
|
end
|
114
143
|
end
|
115
144
|
|
metadata
CHANGED
@@ -1,64 +1,82 @@
|
|
1
|
-
--- !ruby/object:Gem::Specification
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
2
|
name: guillotine
|
3
|
-
version: !ruby/object:Gem::Version
|
4
|
-
|
5
|
-
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
prerelease: false
|
5
|
+
segments:
|
6
|
+
- 1
|
7
|
+
- 3
|
8
|
+
- 0
|
9
|
+
version: 1.3.0
|
6
10
|
platform: ruby
|
7
|
-
authors:
|
11
|
+
authors:
|
8
12
|
- Rick Olson
|
9
13
|
autorequire:
|
10
14
|
bindir: bin
|
11
15
|
cert_chain: []
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
16
|
+
|
17
|
+
date: 2012-08-19 00:00:00 -07:00
|
18
|
+
default_executable:
|
19
|
+
dependencies:
|
20
|
+
- !ruby/object:Gem::Dependency
|
21
|
+
type: :runtime
|
22
|
+
version_requirements: &id001 !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
19
24
|
- - ~>
|
20
|
-
- !ruby/object:Gem::Version
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
segments:
|
27
|
+
- 1
|
28
|
+
- 2
|
29
|
+
- 6
|
21
30
|
version: 1.2.6
|
22
|
-
|
31
|
+
name: sinatra
|
32
|
+
requirement: *id001
|
23
33
|
prerelease: false
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
none: false
|
29
|
-
requirements:
|
34
|
+
- !ruby/object:Gem::Dependency
|
35
|
+
type: :runtime
|
36
|
+
version_requirements: &id002 !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
30
38
|
- - ~>
|
31
|
-
- !ruby/object:Gem::Version
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
segments:
|
41
|
+
- 2
|
42
|
+
- 2
|
43
|
+
- 6
|
32
44
|
version: 2.2.6
|
33
|
-
|
45
|
+
name: addressable
|
46
|
+
requirement: *id002
|
34
47
|
prerelease: false
|
35
|
-
|
36
|
-
- !ruby/object:Gem::Dependency
|
37
|
-
name: rack-test
|
38
|
-
requirement: &70118903830520 !ruby/object:Gem::Requirement
|
39
|
-
none: false
|
40
|
-
requirements:
|
41
|
-
- - ! '>='
|
42
|
-
- !ruby/object:Gem::Version
|
43
|
-
version: '0'
|
48
|
+
- !ruby/object:Gem::Dependency
|
44
49
|
type: :development
|
50
|
+
version_requirements: &id003 !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - ">="
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
segments:
|
55
|
+
- 0
|
56
|
+
version: "0"
|
57
|
+
name: rack-test
|
58
|
+
requirement: *id003
|
45
59
|
prerelease: false
|
46
|
-
version_requirements: *70118903830520
|
47
60
|
description: Adaptable private URL shortener
|
48
61
|
email: technoweenie@gmail.com
|
49
62
|
executables: []
|
63
|
+
|
50
64
|
extensions: []
|
65
|
+
|
51
66
|
extra_rdoc_files: []
|
52
|
-
|
67
|
+
|
68
|
+
files:
|
53
69
|
- CHANGELOG.md
|
54
70
|
- Gemfile
|
55
71
|
- LICENSE
|
56
72
|
- README.md
|
57
73
|
- Rakefile
|
74
|
+
- config/cassandra_config.json
|
58
75
|
- config/haproxy.riak.cfg
|
59
76
|
- guillotine.gemspec
|
60
77
|
- lib/guillotine.rb
|
61
78
|
- lib/guillotine/adapters/active_record_adapter.rb
|
79
|
+
- lib/guillotine/adapters/cassandra_adapter.rb
|
62
80
|
- lib/guillotine/adapters/memory_adapter.rb
|
63
81
|
- lib/guillotine/adapters/mongo_adapter.rb
|
64
82
|
- lib/guillotine/adapters/redis_adapter.rb
|
@@ -70,44 +88,54 @@ files:
|
|
70
88
|
- script/cibuild
|
71
89
|
- test/active_record_adapter_test.rb
|
72
90
|
- test/app_test.rb
|
91
|
+
- test/cassandra_adapter_test.rb
|
73
92
|
- test/helper.rb
|
74
93
|
- test/host_checker_test.rb
|
75
94
|
- test/memory_adapter_test.rb
|
76
95
|
- test/mongo_adapter_test.rb
|
96
|
+
- test/options_test.rb
|
77
97
|
- test/redis_adapter_test.rb
|
78
98
|
- test/riak_adapter_test.rb
|
79
99
|
- test/sequel_adapter_test.rb
|
80
100
|
- test/service_test.rb
|
101
|
+
has_rdoc: true
|
81
102
|
homepage: https://github.com/technoweenie/gitio
|
82
103
|
licenses: []
|
104
|
+
|
83
105
|
post_install_message:
|
84
106
|
rdoc_options: []
|
85
|
-
|
107
|
+
|
108
|
+
require_paths:
|
86
109
|
- lib
|
87
|
-
required_ruby_version: !ruby/object:Gem::Requirement
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
requirements:
|
96
|
-
- -
|
97
|
-
- !ruby/object:Gem::Version
|
98
|
-
|
110
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
111
|
+
requirements:
|
112
|
+
- - ">="
|
113
|
+
- !ruby/object:Gem::Version
|
114
|
+
segments:
|
115
|
+
- 0
|
116
|
+
version: "0"
|
117
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
118
|
+
requirements:
|
119
|
+
- - ">="
|
120
|
+
- !ruby/object:Gem::Version
|
121
|
+
segments:
|
122
|
+
- 0
|
123
|
+
version: "0"
|
99
124
|
requirements: []
|
125
|
+
|
100
126
|
rubyforge_project: guillotine
|
101
|
-
rubygems_version: 1.
|
127
|
+
rubygems_version: 1.3.6
|
102
128
|
signing_key:
|
103
129
|
specification_version: 2
|
104
130
|
summary: Adaptable private URL shortener
|
105
|
-
test_files:
|
131
|
+
test_files:
|
106
132
|
- test/active_record_adapter_test.rb
|
107
133
|
- test/app_test.rb
|
134
|
+
- test/cassandra_adapter_test.rb
|
108
135
|
- test/host_checker_test.rb
|
109
136
|
- test/memory_adapter_test.rb
|
110
137
|
- test/mongo_adapter_test.rb
|
138
|
+
- test/options_test.rb
|
111
139
|
- test/redis_adapter_test.rb
|
112
140
|
- test/riak_adapter_test.rb
|
113
141
|
- test/sequel_adapter_test.rb
|