short 0.5.4 → 0.6.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/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
|