short 0.5.4 → 0.6.0
Sign up to get free protection for your applications and to get access to all the features.
- data/README.md +14 -0
- data/bin/short +3 -0
- data/lib/shortener/configuration.rb +38 -6
- data/lib/shortener/server/Gemfile +2 -0
- data/lib/shortener/server/Gemfile.lock +5 -0
- data/lib/shortener/server/api/v1.rb +76 -0
- data/lib/shortener/server/auth.rb +117 -0
- data/lib/shortener/server/brief.rb +189 -0
- data/lib/shortener/server/config.ru.template +44 -2
- data/lib/shortener/server/helpers.rb +84 -0
- data/lib/shortener/server/pbkdf2.rb +192 -0
- data/lib/shortener/server/public/css/bootstrap-responsive.min.css +9 -3
- data/lib/shortener/server/public/css/bootstrap.min.css +9 -610
- data/lib/shortener/server/public/css/controls.png +0 -0
- data/lib/shortener/server/public/css/loading.gif +0 -0
- data/lib/shortener/server/public/css/shortener.css +23 -2
- data/lib/shortener/server/public/js/bootstrap.min.js +6 -1
- data/lib/shortener/server/public/js/site.js +2 -2
- data/lib/shortener/server/user.rb +131 -0
- data/lib/shortener/server/views.rb +55 -0
- data/lib/shortener/server/views/add.haml +8 -8
- data/lib/shortener/server/views/display.haml +1 -1
- data/lib/shortener/server/views/index.haml +28 -12
- data/lib/shortener/server/views/layout.haml +38 -12
- data/lib/shortener/server/views/s3/layout.haml +1 -1
- data/lib/shortener/server/views/u/edit.haml +40 -0
- data/lib/shortener/server/views/u/login.haml +13 -0
- data/lib/shortener/server/views/upload.haml +9 -9
- data/lib/shortener/server/warden.rb +29 -0
- data/lib/shortener/short.rb +2 -2
- data/lib/shortener/tasks/heroku.rb +16 -10
- data/lib/shortener/version.rb +1 -1
- metadata +22 -12
- data/lib/shortener/server.rb +0 -350
- data/lib/shortener/server/public/flash/clippy.swf +0 -0
data/README.md
CHANGED
@@ -9,6 +9,20 @@ for obvious reasons, the demo is configured with S3 disabled. However, if you ar
|
|
9
9
|
to play around with `short` follow the instructions below and use `http://shortener1.heroku.com`
|
10
10
|
as your the url you want to use.
|
11
11
|
|
12
|
+
## Version 0.6.0
|
13
|
+
|
14
|
+
v0.6.0 is pretty much a complete rewrite of most things short. The server has
|
15
|
+
been divided up in to smaller chunks, seperating the shortening service from the
|
16
|
+
view front-end, and adding an optional authentication layer, and then the short
|
17
|
+
client was patched to use these changes. v0.6.0 will hopefully wind up being
|
18
|
+
pretty close to version 1.0, but before then I want to put together some test
|
19
|
+
cases and rewrite the docs to accuarately reflect the new server structure.
|
20
|
+
|
21
|
+
There shouldn't be any configuration changes most people need to make, unless
|
22
|
+
you want to take advantage of the new features, if you'd like to check these out
|
23
|
+
before I can improve the docs, once again the `Configuration` class will be your
|
24
|
+
friend.
|
25
|
+
|
12
26
|
## Upgrading to 0.5.0
|
13
27
|
|
14
28
|
v0.5.0 updates how shortener stores some data. To assist in keeping your data,
|
data/bin/short
CHANGED
@@ -75,6 +75,7 @@ def show_index
|
|
75
75
|
index = Shortener.index
|
76
76
|
rescue Shortener::NetworkException => boom
|
77
77
|
puts boom.message
|
78
|
+
exit
|
78
79
|
end
|
79
80
|
short_summary = index.map do |v|
|
80
81
|
url = v['url'].length > 38 ? "#{v['url'][0..25]}...#{v['url'][-10..-1]}" : "#{v['url']}"
|
@@ -174,6 +175,8 @@ when 'delete'
|
|
174
175
|
do_action(:delete, ARGV[1])
|
175
176
|
when '-v', '--version'
|
176
177
|
puts "short version #{Shortener::VERSION}"
|
178
|
+
when '-h', '--help'
|
179
|
+
usage
|
177
180
|
else
|
178
181
|
do_action(:shorten, ARGV[0])
|
179
182
|
end
|
@@ -16,9 +16,10 @@ class Shortener
|
|
16
16
|
|
17
17
|
OPTIONS = [:SHORTENER_URL, :DEFAULT_URL, :REDISTOGO_URL, :S3_KEY_PREFIX,
|
18
18
|
:S3_ACCESS_KEY_ID, :S3_SECRET_ACCESS_KEY, :S3_DEFAULT_ACL, :S3_BUCKET,
|
19
|
-
:DOTFILE_PATH, :S3_ENABLED, :SHORTENER_NS
|
19
|
+
:DOTFILE_PATH, :S3_ENABLED, :SHORTENER_NS, :REQUIRE_AUTH, :ALLOW_SIGNUP,
|
20
|
+
:VIEWS]
|
20
21
|
|
21
|
-
HEROKU_IGNORE = [:DOTFILE_PATH, :SHORTENER_URL, :REDISTOGO_URL]
|
22
|
+
HEROKU_IGNORE = [:DOTFILE_PATH, :SHORTENER_URL, :REDISTOGO_URL, :REQUIRE_AUTH]
|
22
23
|
|
23
24
|
END_POINTS = [:add, :fetch, :upload, :index, :delete]
|
24
25
|
|
@@ -33,7 +34,18 @@ class Shortener
|
|
33
34
|
check_env
|
34
35
|
@options = @options.merge!(opts)
|
35
36
|
@options[:DEFAULT_URL] ||= '/index'
|
37
|
+
if @options[:SHORTENER_URL] && @options[:SHORTENER_URL][-1] == '/'
|
38
|
+
@options[:SHORTENER_URL].chop!
|
39
|
+
end
|
36
40
|
@options[:SHORTENER_NS] ||= :shortener
|
41
|
+
@options[:VIEWS] = !(@options[:VIEWS] == false || @options[:VIEWS] == 'false')
|
42
|
+
@options[:ALLOW_SIGNUP] = @options.has_key?(:ALLOW_SIGNUP)
|
43
|
+
if @options[:REQUIRE_AUTH].is_a?(String) || @options[:REQUIRE_AUTH].is_a?(Symbol)
|
44
|
+
@options[:REQUIRE_AUTH] = @options[:REQUIRE_AUTH].to_s.split(',').map do |auth|
|
45
|
+
auth.upcase.to_sym
|
46
|
+
end
|
47
|
+
end
|
48
|
+
@options[:REQUIRE_AUTH] ||= []
|
37
49
|
else
|
38
50
|
@options = Configuration.current.options
|
39
51
|
end
|
@@ -53,11 +65,20 @@ class Shortener
|
|
53
65
|
def to_params
|
54
66
|
ret = Array.new
|
55
67
|
@options.each {|k,v| ret << "#{k}=#{v}" unless HEROKU_IGNORE.include?(k)}
|
68
|
+
_ra = @options[:REQUIRE_AUTH].join(',')
|
69
|
+
ret << "REQUIRE_AUTH=#{_ra}"
|
56
70
|
ret.join(" ")
|
57
71
|
end
|
58
72
|
|
73
|
+
def to_json
|
74
|
+
safe_options = @options.delete_if do |k|
|
75
|
+
[:S3_SECRET_ACCESS_KEY, :S3_ACCESS_KEY_ID, :REDISTOGO_URL].include?(k)
|
76
|
+
end
|
77
|
+
safe_options.to_json
|
78
|
+
end
|
79
|
+
|
59
80
|
OPTIONS.each do |opt|
|
60
|
-
next if [:REDISTOGO_URL, :S3_ENABLED].include?(opt)
|
81
|
+
next if [:REDISTOGO_URL, :S3_ENABLED, :REQUIRES_AUTH].include?(opt)
|
61
82
|
method_name = opt.to_s.downcase.to_sym
|
62
83
|
define_method "#{method_name}" do
|
63
84
|
@options[opt]
|
@@ -128,6 +149,19 @@ class Shortener
|
|
128
149
|
OpenSSL::Digest::Digest.new('sha1'), s3_secret_access_key, policy)).gsub("\n","")
|
129
150
|
end
|
130
151
|
|
152
|
+
# a boolean indicating whether we should use authentication
|
153
|
+
def authenticate?
|
154
|
+
!(@options[:REQUIRE_AUTH] == [])
|
155
|
+
end
|
156
|
+
|
157
|
+
# check if this endpoint needs auth
|
158
|
+
def auth_route?(url)
|
159
|
+
ep = url.split('/').last
|
160
|
+
return false if ep.nil?
|
161
|
+
ep.gsub!('.json', '')
|
162
|
+
@options[:REQUIRE_AUTH].include?(ep.upcase.to_sym)
|
163
|
+
end
|
164
|
+
|
131
165
|
private
|
132
166
|
|
133
167
|
# Parse the YAML dotfile if one exists
|
@@ -150,10 +184,8 @@ class Shortener
|
|
150
184
|
case end_point
|
151
185
|
when :fetch
|
152
186
|
opts
|
153
|
-
when :delete
|
154
|
-
"#{end_point}/#{opts}"
|
155
187
|
else
|
156
|
-
end_point
|
188
|
+
"api/v1/#{end_point}"
|
157
189
|
end
|
158
190
|
end
|
159
191
|
|
@@ -8,11 +8,14 @@ GEM
|
|
8
8
|
redis (2.2.2)
|
9
9
|
redis-namespace (1.1.0)
|
10
10
|
redis (< 3.0.0)
|
11
|
+
ruby-hmac (0.4.0)
|
11
12
|
sinatra (1.3.1)
|
12
13
|
rack (~> 1.3, >= 1.3.4)
|
13
14
|
rack-protection (~> 1.1, >= 1.1.2)
|
14
15
|
tilt (~> 1.3, >= 1.3.3)
|
15
16
|
tilt (1.3.3)
|
17
|
+
warden (1.1.1)
|
18
|
+
rack (>= 1.0)
|
16
19
|
|
17
20
|
PLATFORMS
|
18
21
|
ruby
|
@@ -20,4 +23,6 @@ PLATFORMS
|
|
20
23
|
DEPENDENCIES
|
21
24
|
haml
|
22
25
|
redis-namespace
|
26
|
+
ruby-hmac
|
23
27
|
sinatra
|
28
|
+
warden
|
@@ -0,0 +1,76 @@
|
|
1
|
+
require 'uri'
|
2
|
+
require 'json'
|
3
|
+
require 'digest/sha1'
|
4
|
+
require 'base64'
|
5
|
+
require 'haml'
|
6
|
+
|
7
|
+
class Shortener
|
8
|
+
module Server
|
9
|
+
module Api
|
10
|
+
class V1 < Sinatra::Base
|
11
|
+
|
12
|
+
set(:s3_available) { |v| condition {$conf.s3_available == v} }
|
13
|
+
set(:allow_signup) { |v| condition {$conf.allow_signup} }
|
14
|
+
|
15
|
+
set :root, File.dirname(File.dirname(__FILE__))
|
16
|
+
|
17
|
+
helpers ShortServerHelpers
|
18
|
+
|
19
|
+
before do
|
20
|
+
if $conf.auth_route?(env['PATH_INFO']) &&
|
21
|
+
!env['warden'].authenticated?(:token) &&
|
22
|
+
!env['warden'].authenticate!(:token)
|
23
|
+
halt 401, {}, "Not Authorized, specify your auth token."
|
24
|
+
end
|
25
|
+
end if $conf.authenticate?
|
26
|
+
|
27
|
+
get '/' do
|
28
|
+
redirect $conf.default_url
|
29
|
+
end
|
30
|
+
|
31
|
+
get '/api/v1/index.?:format?' do
|
32
|
+
Brief.all.to_json
|
33
|
+
end
|
34
|
+
|
35
|
+
post '/api/v1/upload.?:format?' do
|
36
|
+
@data = Brief.upload(params['shortener'])
|
37
|
+
puts "set #{@data['shortened']} to #{params['shortener']['file_name']}"
|
38
|
+
@url = "#{base_url}/#{@data['shortened']}"
|
39
|
+
content_type :json
|
40
|
+
@data.merge({html: haml(:display, layout: false)}).to_json
|
41
|
+
end
|
42
|
+
|
43
|
+
post '/api/v1/add.?:format?' do
|
44
|
+
@data = Brief.shorten(params["shortener"])
|
45
|
+
@url = "#{base_url}/#{@data['shortened']}"
|
46
|
+
puts "set #{@url} to #{params['shortener']['url']}"
|
47
|
+
content_type :json
|
48
|
+
@data.merge({success: true, html: haml(:display, :layout => false)}).to_json
|
49
|
+
end
|
50
|
+
|
51
|
+
post '/api/v1/delete.?:format?' do
|
52
|
+
status = Brief.delete(params['id'])
|
53
|
+
nope! "Short not found: #{params['id']}" if status == false
|
54
|
+
puts " - deleted short id: #{params['id']}"
|
55
|
+
content_type :json
|
56
|
+
{success: status, shortened: params['id']}.to_json
|
57
|
+
end
|
58
|
+
|
59
|
+
get '/api/v1/config.?:format?' do
|
60
|
+
$conf.to_json
|
61
|
+
end
|
62
|
+
|
63
|
+
#get '/:id.?:format?' do
|
64
|
+
get %r{\/([a-z0-9]{3,})(\.[a-z]{3,}){0,1}}i do
|
65
|
+
id = params[:captures].first
|
66
|
+
@short, type = Brief.find(id, params)
|
67
|
+
redirect @short if type == :url
|
68
|
+
return haml(:"s3/#{@short['type']}", layout: :'s3/layout') if type == :s3
|
69
|
+
content_type :json
|
70
|
+
@short
|
71
|
+
end
|
72
|
+
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
@@ -0,0 +1,117 @@
|
|
1
|
+
require File.join(File.dirname(__FILE__), 'user')
|
2
|
+
|
3
|
+
class Shortener
|
4
|
+
module Server
|
5
|
+
class Auth < Sinatra::Base
|
6
|
+
|
7
|
+
dir = File.expand_path(File.dirname(__FILE__))
|
8
|
+
set :root, dir
|
9
|
+
set :public_folder, File.join(dir, 'public')
|
10
|
+
|
11
|
+
set(:signup) {|v| condition {$conf.allow_signup == v}}
|
12
|
+
set(:has_views) {|v| condition {$conf.views == v}}
|
13
|
+
|
14
|
+
helpers ShortServerHelpers
|
15
|
+
|
16
|
+
helpers do
|
17
|
+
def _response(views, api)
|
18
|
+
if $conf.views && !(params[:format] == '.json' ||
|
19
|
+
request.env['REQUEST_PATH'].include?('.json'))
|
20
|
+
return views.call if views.is_a?(Proc)
|
21
|
+
redirect views
|
22
|
+
else
|
23
|
+
content_type :json
|
24
|
+
api.is_a?(Proc) ? api.call.to_json : api.to_json
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
# this is our failure app, and this is where we handle that.
|
30
|
+
post '/unauthenticated/?' do
|
31
|
+
status 401
|
32
|
+
_response(->{haml :'u/login'},
|
33
|
+
{message: "You are not authenticated. Pass your token!"})
|
34
|
+
end
|
35
|
+
|
36
|
+
#
|
37
|
+
# these handle view based authentication.
|
38
|
+
#
|
39
|
+
|
40
|
+
get('/u/login', has_views: true) { haml :'u/login'}
|
41
|
+
|
42
|
+
get('/u/edit', has_views: true) do
|
43
|
+
authorize!
|
44
|
+
@action = '/api/v1/u/update'
|
45
|
+
@user = env['warden'].user
|
46
|
+
haml :'u/edit'
|
47
|
+
end
|
48
|
+
|
49
|
+
get('/u/signup', signup: true, has_views: true) do
|
50
|
+
@action = '/api/v1/u/create'
|
51
|
+
@user = User.new
|
52
|
+
haml :'u/edit'
|
53
|
+
end
|
54
|
+
|
55
|
+
#
|
56
|
+
# These are the api calls associated with authentiction
|
57
|
+
#
|
58
|
+
|
59
|
+
get '/api/v1/u/username_available.json' do
|
60
|
+
{available: User.available?(params['user']['username'])}.to_json
|
61
|
+
end
|
62
|
+
|
63
|
+
post '/api/v1/u/create', signup: true do
|
64
|
+
ret = if User.available?(params['user']['username'])
|
65
|
+
u = User.new(params['user'])
|
66
|
+
u.save
|
67
|
+
env['warden'].set_user(u)
|
68
|
+
["#{base_url}/v/index", u]
|
69
|
+
else
|
70
|
+
["#{base_url}/u/signup", {status: :fail, message: 'Username not available'}]
|
71
|
+
end
|
72
|
+
_response(*ret)
|
73
|
+
end
|
74
|
+
|
75
|
+
post '/api/v1/u/login.?:format?' do
|
76
|
+
env['warden'].authenticate!#(:token)
|
77
|
+
_url = session.delete(:REDIRECT_TO) || "#{$conf.shortener_url}/v/index"
|
78
|
+
_response(_url, env['warden'].user)
|
79
|
+
end
|
80
|
+
|
81
|
+
get '/api/v1/u/logout.?:format?' do
|
82
|
+
env['warden'].logout if env['warden'].authenticated?
|
83
|
+
_response($conf.default_url, {message: 'l8er'})
|
84
|
+
end
|
85
|
+
|
86
|
+
post '/api/v1/u/update' do
|
87
|
+
authorize!
|
88
|
+
@user = env['warden'].user
|
89
|
+
[:username, :email, :name].each do |attr|
|
90
|
+
@user.send(:"#{attr}=", params['user'][attr.to_s])
|
91
|
+
end
|
92
|
+
unless params['user']['password'].nil? || params['user']['password'].empty?
|
93
|
+
@user.password = params['user']['password']
|
94
|
+
end
|
95
|
+
@user.save
|
96
|
+
_response("#{base_url}/v/index", @user)
|
97
|
+
end
|
98
|
+
|
99
|
+
post '/api/v1/u/reset_token.json' do
|
100
|
+
authorize!
|
101
|
+
user = env['warden'].user
|
102
|
+
user.reset_token
|
103
|
+
user.save
|
104
|
+
env['warden'].set_user(user)
|
105
|
+
content_type :json
|
106
|
+
env['warden'].user.to_json
|
107
|
+
end
|
108
|
+
|
109
|
+
post '/api/v1/u/delete' do
|
110
|
+
env['warden'].user.delete
|
111
|
+
env['warden'].logout(env['warden'].config.default_scope)
|
112
|
+
_response($conf.default_url, {message: "Miss you already"})
|
113
|
+
end
|
114
|
+
|
115
|
+
end
|
116
|
+
end
|
117
|
+
end
|
@@ -0,0 +1,189 @@
|
|
1
|
+
|
2
|
+
class Shortener
|
3
|
+
module Server
|
4
|
+
class Brief
|
5
|
+
|
6
|
+
class << self
|
7
|
+
|
8
|
+
def all
|
9
|
+
$redis.keys('data:*').map do |key|
|
10
|
+
short = $redis.hgetall(key)
|
11
|
+
puts " url for #{key[-5..-1]} => #{short['url']}"
|
12
|
+
short['expire'] = $redis.ttl(short['expire']) if short.has_key?('expire')
|
13
|
+
short
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
def find(id, params)
|
18
|
+
sha = $redis.get(id)
|
19
|
+
if sha.nil? # => Short Not Found
|
20
|
+
if (params[:captures].last == '.json')
|
21
|
+
nope! "Short not found: #{id}"
|
22
|
+
else
|
23
|
+
puts "redirecting #{params[:captures].inspect} to default url"
|
24
|
+
return $conf.default_url, :url
|
25
|
+
end
|
26
|
+
else # => Short Found
|
27
|
+
key = "data:#{sha}:#{id}"
|
28
|
+
short = $redis.hgetall(key)
|
29
|
+
not_expired = short.has_key?('expire') ? $redis.get(short['expire']) : true
|
30
|
+
not_maxed = !(short['click_count'].to_i >= short['max_clicks'].to_i)
|
31
|
+
short.has_key?('max_clicks') ? not_maxed : not_maxed = true
|
32
|
+
if params[:captures].last == '.json' # => We just want JSON
|
33
|
+
ret = short.merge({expired: not_expired.nil? , maxed: !not_maxed})
|
34
|
+
return ret.to_json, :json
|
35
|
+
else # => Redirect Me!
|
36
|
+
$redis.hincrby(key, 'click_count', 1) if not_expired && not_maxed
|
37
|
+
if not_expired
|
38
|
+
unless short['s3'] == 'true' && !(short['type'] == 'download')
|
39
|
+
if not_maxed
|
40
|
+
puts "redirecting found short #{id} to #{short['url']}"
|
41
|
+
return short['url'], :url
|
42
|
+
end # => max clicks check
|
43
|
+
else # => This is S3 content
|
44
|
+
puts "rendering view for s3 content. #{id} => #{short['url']}"
|
45
|
+
return short, :s3
|
46
|
+
end # => it's S3 and needs displaying.
|
47
|
+
end # => expired check
|
48
|
+
end # => format
|
49
|
+
end
|
50
|
+
# => short was maxed or expired & not a JSON request
|
51
|
+
return $conf.default_url, :url
|
52
|
+
end
|
53
|
+
|
54
|
+
def shorten(params)
|
55
|
+
bad! 'Missing url.' unless url = params['url']
|
56
|
+
bad! 'Bad URL' unless params['url'] =~ /(^http|^www)/
|
57
|
+
url = "http://#{url}" unless /^http/i =~ url
|
58
|
+
bad! 'Bad URL' unless (url = URI.parse(url)) && /^http/ =~ url.scheme
|
59
|
+
|
60
|
+
%w(max_clicks expire desired_short allow_override).each do |k|
|
61
|
+
params[k] = false if params[k].nil? || params[k].empty?
|
62
|
+
end
|
63
|
+
|
64
|
+
unless params['max_clicks'] || params['expire'] || params['desired_short']
|
65
|
+
data = check_cache(url)
|
66
|
+
end
|
67
|
+
data ||= get_short_key(url, params)
|
68
|
+
|
69
|
+
data
|
70
|
+
end
|
71
|
+
|
72
|
+
def delete(id)
|
73
|
+
sha = $redis.get(id)
|
74
|
+
unless sha.nil?
|
75
|
+
$redis.multi do
|
76
|
+
$redis.del "data:#{sha}:#{id}"
|
77
|
+
$redis.del "expire:#{sha}:#{id}"
|
78
|
+
$redis.del id
|
79
|
+
end
|
80
|
+
true
|
81
|
+
else
|
82
|
+
false
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
def upload(params)
|
87
|
+
bad! 'Missing content type.' unless type = params['type']
|
88
|
+
fname = params['file_name'].gsub(' ', '+')
|
89
|
+
url = "https://s3.amazonaws.com/#{$conf.s3_bucket}/#{$conf.s3_key_prefix}/#{fname}"
|
90
|
+
data = {'s3' => true, 'extension' => File.extname(fname)[1..-1],
|
91
|
+
'description' => params.delete('description'),
|
92
|
+
'name' => params.delete('name'), 'type' => params.delete('type')}
|
93
|
+
get_short_key(url, params, data)
|
94
|
+
end
|
95
|
+
|
96
|
+
private
|
97
|
+
|
98
|
+
def get_short_key(url, options = {}, data = {})
|
99
|
+
hsh_data = catch :stop_setting_up do
|
100
|
+
unless options['desired_short']
|
101
|
+
puts " just generating a short"
|
102
|
+
key = generate_short
|
103
|
+
else
|
104
|
+
do_check = $redis.get(options['desired_short'])
|
105
|
+
key = if do_check.nil? || passes_desired_short_check(url, do_check, options)
|
106
|
+
options['desired_short']
|
107
|
+
else
|
108
|
+
bad! 'Name is already taken. Use Allow override' unless options['allow_override'] == 'true'
|
109
|
+
generate_short
|
110
|
+
end
|
111
|
+
end
|
112
|
+
|
113
|
+
sha = Digest::SHA1.hexdigest(url.to_s)
|
114
|
+
$redis.set(key, sha)
|
115
|
+
|
116
|
+
hsh_data = data.merge('shortened' => key, 'url' => url, 'set_count' => 1)
|
117
|
+
hsh_data['max_clicks'] = options['max_clicks'].to_i if options['max_clicks']
|
118
|
+
|
119
|
+
if options['expire'] # set expire time if specified
|
120
|
+
ttl = options['expire'].to_i
|
121
|
+
ttl_key = "expire:#{sha}:#{key}"
|
122
|
+
$redis.set(ttl_key, "#{sha}:#{key}")
|
123
|
+
$redis.expire(ttl_key, ttl)
|
124
|
+
hsh_data[:expire] = ttl_key
|
125
|
+
end
|
126
|
+
$redis.hmset("data:#{sha}:#{key}", *arrayify_hash(hsh_data))
|
127
|
+
|
128
|
+
hsh_data
|
129
|
+
end
|
130
|
+
end
|
131
|
+
|
132
|
+
def bad! message
|
133
|
+
throw :halt, [412, {}, message]
|
134
|
+
end
|
135
|
+
|
136
|
+
def nope!(message = 'No luck.')
|
137
|
+
throw :halt, [404, {}, message]
|
138
|
+
end
|
139
|
+
|
140
|
+
def generate_short
|
141
|
+
begin
|
142
|
+
o = [('a'..'z'),('A'..'Z'),(0..9)].map{|i| i.to_a}.flatten;
|
143
|
+
key = (0..4).map{ o[rand(o.length)] }.join;
|
144
|
+
puts "testing #{key}"
|
145
|
+
end while !$redis.get(key).nil?
|
146
|
+
key
|
147
|
+
end
|
148
|
+
|
149
|
+
def passes_desired_short_check(url, check, options)
|
150
|
+
check_key = "data:#{check}:#{options['desired_short']}"
|
151
|
+
prev_set = $redis.hgetall(check_key)
|
152
|
+
|
153
|
+
return false if prev_set['expire']
|
154
|
+
|
155
|
+
# if we don't expire or have max clicks and previously set key
|
156
|
+
# doesn't expire or have max clicks we can go ahead and use it
|
157
|
+
# without any further setup.
|
158
|
+
unless options['expire'] || options['max_clicks']
|
159
|
+
if (!prev_set['max_clicks'] && !prev_set['expire'] &&
|
160
|
+
(prev_set['url'] == url.to_s))
|
161
|
+
$redis.hincrby(check_key, 'set_count', 1)
|
162
|
+
throw :stop_setting_up, prev_set
|
163
|
+
end
|
164
|
+
end
|
165
|
+
|
166
|
+
return (prev_set['clicks'].to_i > prev_set['max_clicks'].to_i)
|
167
|
+
end
|
168
|
+
|
169
|
+
def check_cache(url)
|
170
|
+
sha = Digest::SHA1.hexdigest(url.to_s)
|
171
|
+
|
172
|
+
$redis.keys("data:#{sha}:*").each do |key|
|
173
|
+
short = $redis.hgetall(key)
|
174
|
+
unless short == {} || short['expire'] || short['max_clicks']
|
175
|
+
$redis.hincrby(key, 'set_count', 1)
|
176
|
+
return short
|
177
|
+
end
|
178
|
+
end
|
179
|
+
nil
|
180
|
+
end
|
181
|
+
|
182
|
+
def arrayify_hash(hsh)
|
183
|
+
hsh.keys.map {|k| [k, hsh[k]] }.flatten
|
184
|
+
end
|
185
|
+
end # => class << self
|
186
|
+
|
187
|
+
end # => Shirt
|
188
|
+
end # => Server
|
189
|
+
end # => Shortener
|