short 0.3.3 → 0.4.0
Sign up to get free protection for your applications and to get access to all the features.
- data/Guardfile +14 -0
- data/README.md +79 -11
- data/bin/short +8 -5
- data/lib/shortener/configuration.rb +41 -5
- data/lib/shortener/server/views/display.haml +2 -1
- data/lib/shortener/server/views/layout.haml +1 -1
- data/lib/shortener/server/views/upload.haml +2 -2
- data/lib/shortener/server.rb +29 -23
- data/lib/shortener/short.rb +147 -0
- data/lib/shortener/version.rb +1 -1
- data/lib/shortener.rb +5 -2
- data/tasks/heroku.rake +1 -1
- data/test/test_short.rb +68 -0
- metadata +14 -13
- data/lib/shortener/client.rb +0 -69
- data/test/test_client.rb +0 -26
data/Guardfile
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
# A sample Guardfile
|
2
|
+
# More info at https://github.com/guard/guard#readme
|
3
|
+
|
4
|
+
guard 'minitest' do
|
5
|
+
# with Minitest::Unit
|
6
|
+
watch(%r|^test/test_(.*)\.rb|)
|
7
|
+
watch(%r|^lib/shortener/(.*)\.rb|) { |m| "test/test_#{m[1]}.rb" }
|
8
|
+
watch(%r|^test/test_helper\.rb|) { "test" }
|
9
|
+
|
10
|
+
# with Minitest::Spec
|
11
|
+
# watch(%r|^spec/(.*)_spec\.rb|)
|
12
|
+
# watch(%r|^lib/(.*)\.rb|) { |m| "spec/#{m[1]}_spec.rb" }
|
13
|
+
# watch(%r|^spec/spec_helper\.rb|) { "spec" }
|
14
|
+
end
|
data/README.md
CHANGED
@@ -5,24 +5,89 @@ A super simple, Sinatra based, Redis backed URL shortener designed to be deploye
|
|
5
5
|
|
6
6
|
Check it out [here](http://shortener1.heroku.com), but be aware that the css on the add page that displays the shortened link assumes you're using a short url, so it kind of looks like shit when it displays `http://shortener1.heroku.com/whatev`
|
7
7
|
|
8
|
-
|
8
|
+
for obvious reasons, the demo is configured with S3 disabled. However, if you are just looking
|
9
|
+
to play around with `short` follow the instructions below and use `http://shortener1.heroku.com`
|
10
|
+
as your the url you want to use.
|
9
11
|
|
10
|
-
###
|
12
|
+
### Installation
|
11
13
|
|
12
|
-
|
14
|
+
is now as easy as
|
15
|
+
|
16
|
+
`gem install short`
|
17
|
+
|
18
|
+
and
|
19
|
+
|
20
|
+
`short`
|
21
|
+
|
22
|
+
which will then prompt you to supply the config vars necessary to use the short
|
23
|
+
executable or the short server. You can check out `/lib/shortener/configuration.rb`
|
24
|
+
to see a list of available configuration variables.
|
25
|
+
|
26
|
+
configuration variables are parsed by default on-load and cached for each use,
|
27
|
+
but all client methods allow override.
|
28
|
+
|
29
|
+
### Server
|
30
|
+
|
31
|
+
Shortener server provides a primitive API for interacting with shorts. You get the following.
|
13
32
|
|
14
|
-
for the `short` client to work, you only need something like
|
15
33
|
<pre>
|
16
|
-
|
17
|
-
|
34
|
+
get '/index.json' => info on all shorts.
|
35
|
+
|
36
|
+
get '/delete/:id.json' => delete short @ id, returns {success: true, shortened: id}
|
37
|
+
|
38
|
+
get '/:id.json' => data hash for short @ id
|
39
|
+
|
40
|
+
post '/add.json' => data hash for new short
|
41
|
+
can accept the following options:
|
42
|
+
url: the url to shorten
|
43
|
+
expire: time in seconds that this short should live
|
44
|
+
max-clicks: the maximum number of clicks this short should accept.
|
45
|
+
desired-short: a short that should be set for this url.
|
46
|
+
allow-override: if desired-short is passed, whether or not to allow
|
47
|
+
a random short override.
|
48
|
+
|
49
|
+
post '/upload.json' => data hash for new short,
|
50
|
+
|
51
|
+
a data hash will contain the following keys:
|
52
|
+
|
53
|
+
shortened => id of this short. i.e. 'xZ147'
|
54
|
+
url => url of this short. i.e. 'www.google.com'
|
55
|
+
set-count => number of times this url has been shortened.
|
56
|
+
click-count => number of times this short has been resolved.
|
57
|
+
|
58
|
+
a data hash might contain the following keys:
|
59
|
+
|
60
|
+
expire => expire key that will be checked to see if this key will expire
|
61
|
+
max-clicks => maximum number of clicks this short will resolve for
|
62
|
+
|
63
|
+
if it's an endpoint that performs an action it will have a success key set to true or false.
|
64
|
+
(right now this is stupid and is set to true always, unless it errors, in which case you
|
65
|
+
get a 500 server error. hopefully that changes with some better error messages.)
|
66
|
+
|
67
|
+
|
68
|
+
S3 Keys
|
69
|
+
|
70
|
+
S3 => true if this is S3 content
|
71
|
+
extension => the file extension of S3 content. i.e. 'm4v'
|
72
|
+
file\_name => the name of the file. i.e. '1234.m4v'
|
73
|
+
name => the descriptive name. i.e. 'Pandas'
|
74
|
+
description => the description. i.e. 'A panda sneezes'
|
18
75
|
</pre>
|
19
76
|
|
20
|
-
|
77
|
+
### Client
|
78
|
+
|
79
|
+
`Shortener::Short` provides methods to access the server. You can access it through
|
80
|
+
the `Shortener` class or directly through the `Shortener::Short` class. You get:
|
21
81
|
|
22
|
-
|
23
|
-
|
82
|
+
* shorten
|
83
|
+
* index
|
84
|
+
* fetch
|
85
|
+
* delete
|
24
86
|
|
25
|
-
|
87
|
+
methods, each of which will return a/n istance of the `Short` class which will
|
88
|
+
parse the data and provide some defaults and access to said data.
|
89
|
+
|
90
|
+
### Executable
|
26
91
|
|
27
92
|
Use the `short` executable to
|
28
93
|
|
@@ -44,12 +109,15 @@ Run `short rake -T` to see more info.
|
|
44
109
|
|
45
110
|
### License
|
46
111
|
|
112
|
+
Short makes use of a number of libraries, each of which has its own license.
|
113
|
+
|
114
|
+
Short uses the DWTFYWPL.
|
47
115
|
|
48
116
|
<pre>
|
49
117
|
DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE
|
50
118
|
Version 2, December 2004
|
51
119
|
|
52
|
-
Copyright (C) 20011 Jake Wilkins
|
120
|
+
Copyright (C) 20011 Jake Wilkins \<jake AT jakewilkins DOT com\>
|
53
121
|
|
54
122
|
Everyone is permitted to copy and distribute verbatim or modified
|
55
123
|
copies of this license document, and changing it is allowed as long
|
data/bin/short
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
#!/usr/bin/env ruby
|
2
2
|
$LOAD_PATH.unshift File.expand_path(File.dirname(__FILE__) + '/../lib')
|
3
3
|
|
4
|
-
require 'shortener
|
4
|
+
require 'shortener'
|
5
5
|
|
6
6
|
def start_web
|
7
7
|
begin
|
@@ -27,18 +27,19 @@ def start_web
|
|
27
27
|
end
|
28
28
|
|
29
29
|
def shorten(url)
|
30
|
-
puts Shortener
|
30
|
+
puts Shortener.shorten(url).short_url
|
31
31
|
end
|
32
32
|
|
33
33
|
def fetch(url)
|
34
34
|
if url =~ /http:/
|
35
35
|
url = url[-6..-1]
|
36
36
|
end
|
37
|
-
fetched = Shortener
|
37
|
+
fetched = Shortener.fetch(url)
|
38
38
|
unless fetched['success'] == false
|
39
39
|
puts <<-EOF
|
40
40
|
short => #{fetched['shortened']}
|
41
41
|
url => #{fetched['url']}
|
42
|
+
short-url => #{fetched.short_url}
|
42
43
|
set-count => #{fetched['set-count']}
|
43
44
|
click-count => #{fetched['click-count']}
|
44
45
|
expired => #{fetched['expired']}
|
@@ -50,12 +51,12 @@ def fetch(url)
|
|
50
51
|
end
|
51
52
|
|
52
53
|
def delete(short)
|
53
|
-
del = Shortener
|
54
|
+
del = Shortener.delete(short)
|
54
55
|
puts "#{short} deleted" if del['success']
|
55
56
|
end
|
56
57
|
|
57
58
|
def show_index
|
58
|
-
index = Shortener
|
59
|
+
index = Shortener.index
|
59
60
|
short_summary = index.map do |v|
|
60
61
|
url = v['url'].length > 38 ? "#{v['url'][0..25]}...#{v['url'][-10..-1]}" : "#{v['url']}"
|
61
62
|
"#{v['shortened']} : #{url} #{v['type'].nil? ? '' : ('type: ' + v['type'])}"
|
@@ -151,6 +152,8 @@ when 'rake'
|
|
151
152
|
do_action(:rake, ARGV[1])
|
152
153
|
when 'delete'
|
153
154
|
do_action(:delete, ARGV[1])
|
155
|
+
when '-v', '--version'
|
156
|
+
puts "short version #{Shortener::VERSION}"
|
154
157
|
else
|
155
158
|
do_action(:shorten, ARGV[0])
|
156
159
|
end
|
@@ -5,6 +5,14 @@ class Shortener
|
|
5
5
|
# The class for storing Configuration Information
|
6
6
|
class Configuration
|
7
7
|
|
8
|
+
class << self
|
9
|
+
def current
|
10
|
+
@current_configuration ||= Configuration.new(conf_current_placeholder: nil)
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
attr_accessor :options
|
15
|
+
|
8
16
|
OPTIONS = [:SHORTENER_URL, :DEFAULT_URL, :REDISTOGO_URL, :S3_KEY_PREFIX,
|
9
17
|
:S3_ACCESS_KEY_ID, :S3_SECRET_ACCESS_KEY, :S3_DEFAULT_ACL, :S3_BUCKET,
|
10
18
|
:DOTFILE_PATH, :S3_ENABLED]
|
@@ -17,13 +25,19 @@ class Shortener
|
|
17
25
|
# priority goes dotfile < env < passed option
|
18
26
|
def initialize(opts = Hash.new)
|
19
27
|
# TODO check keys by calls opts.delete {|k| !OPTIONS.include?(k)
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
28
|
+
unless opts.empty?
|
29
|
+
opts.delete(:conf_current_placeholder)
|
30
|
+
@options = Hash.new
|
31
|
+
check_dotfile
|
32
|
+
check_env
|
33
|
+
@options = @options.merge!(opts)
|
34
|
+
@options[:DEFAULT_URL] ||= '/index'
|
35
|
+
else
|
36
|
+
@options = Configuration.current.options
|
37
|
+
end
|
25
38
|
end
|
26
39
|
|
40
|
+
# return a URI for an endpoint based on this configuration
|
27
41
|
def uri_for(end_point, opts = nil)
|
28
42
|
if END_POINTS.include?(end_point.to_sym)
|
29
43
|
path = path_for(end_point, opts)
|
@@ -33,6 +47,7 @@ class Shortener
|
|
33
47
|
end
|
34
48
|
end
|
35
49
|
|
50
|
+
# return a string ENV's for command line use.
|
36
51
|
def to_params
|
37
52
|
ret = Array.new
|
38
53
|
@options.each {|k,v| ret << "#{k}=#{v}" unless HEROKU_IGNORE.include?(k)}
|
@@ -47,6 +62,7 @@ class Shortener
|
|
47
62
|
end
|
48
63
|
end
|
49
64
|
|
65
|
+
# return the URI for the redistogo url
|
50
66
|
def redistogo_url
|
51
67
|
begin
|
52
68
|
URI.parse(@options[:REDISTOGO_URL])
|
@@ -58,10 +74,27 @@ class Shortener
|
|
58
74
|
end
|
59
75
|
end
|
60
76
|
|
77
|
+
# return a boolean of the S3_ENABLED option
|
61
78
|
def s3_enabled
|
62
79
|
@options[:S3_ENABLED].to_s == 'true'
|
63
80
|
end
|
64
81
|
|
82
|
+
# are the necessary options present for S3 to work?
|
83
|
+
def s3_configured
|
84
|
+
ret = true
|
85
|
+
[:S3_KEY_PREFIX, :S3_ACCESS_KEY_ID, :S3_SECRET_ACCESS_KEY,
|
86
|
+
:S3_DEFAULT_ACL, :S3_BUCKET ].each do |k|
|
87
|
+
ret = !@options[k].nil? unless ret == false
|
88
|
+
end
|
89
|
+
ret
|
90
|
+
end
|
91
|
+
|
92
|
+
# is S3 enabled and configured?
|
93
|
+
def s3_available
|
94
|
+
s3_enabled && s3_configured
|
95
|
+
end
|
96
|
+
|
97
|
+
# build an S3 policy.
|
65
98
|
def s3_policy
|
66
99
|
expiration_date = (Time.now + 36000).utc.strftime('%Y-%m-%dT%H:%M:%S.000Z') # 10.hours.from_now
|
67
100
|
max_filesize = 2147483648 # 2.gigabyte
|
@@ -79,6 +112,7 @@ class Shortener
|
|
79
112
|
).gsub(/\n|\r/, '')
|
80
113
|
end
|
81
114
|
|
115
|
+
# Sign an S3 policy
|
82
116
|
def s3_signature(policy)
|
83
117
|
signature = Base64.encode64(OpenSSL::HMAC.digest(
|
84
118
|
OpenSSL::Digest::Digest.new('sha1'), s3_secret_access_key, policy)).gsub("\n","")
|
@@ -86,6 +120,7 @@ class Shortener
|
|
86
120
|
|
87
121
|
private
|
88
122
|
|
123
|
+
# Parse the YAML dotfile if one exists
|
89
124
|
def check_dotfile
|
90
125
|
dotfile = @options[:DOTFILE_PATH] || File.join(ENV['HOME'], ".shortener")
|
91
126
|
if File.exists?(dotfile)
|
@@ -93,6 +128,7 @@ class Shortener
|
|
93
128
|
end
|
94
129
|
end
|
95
130
|
|
131
|
+
# Check our environment for any overrides.
|
96
132
|
def check_env
|
97
133
|
OPTIONS.each do |opt|
|
98
134
|
@options[opt] = ENV[opt.to_s] unless ENV[opt.to_s].nil?
|
@@ -63,7 +63,7 @@
|
|
63
63
|
// async: false,
|
64
64
|
// });
|
65
65
|
$.post('/upload.json', values, function(data){
|
66
|
-
$('#shortener-display').
|
66
|
+
$('#shortener-display').html(data.html)
|
67
67
|
.removeClass('hide');
|
68
68
|
});
|
69
69
|
|
@@ -102,7 +102,7 @@
|
|
102
102
|
%li.radio
|
103
103
|
%input{name: 'shortener[type]', type: 'radio', value: 'download'} Download
|
104
104
|
.row
|
105
|
-
|
105
|
+
#shortener-display.offset8.hide
|
106
106
|
.field
|
107
107
|
%label{for: 'shorterner[name]'} Name
|
108
108
|
%input{type: 'text', name: 'shortener[name]'}
|
data/lib/shortener/server.rb
CHANGED
@@ -22,7 +22,7 @@ class Shortener
|
|
22
22
|
$redis = Redis::Namespace.new(:shortener, redis: _redis)
|
23
23
|
end
|
24
24
|
|
25
|
-
set(:
|
25
|
+
set(:s3_available) {|v| condition {$conf.s3_available == v}}
|
26
26
|
|
27
27
|
helpers do
|
28
28
|
|
@@ -75,11 +75,11 @@ class Shortener
|
|
75
75
|
end
|
76
76
|
|
77
77
|
unless params['max-clicks'] || params['expire'] || params['desired-short']
|
78
|
-
|
78
|
+
data = check_cache(url)
|
79
79
|
end
|
80
|
-
|
80
|
+
data ||= shorten(url, params)
|
81
81
|
|
82
|
-
|
82
|
+
data
|
83
83
|
end
|
84
84
|
|
85
85
|
def set_upload_short(params)
|
@@ -90,13 +90,14 @@ class Shortener
|
|
90
90
|
hash_key = "data:#{sha}:#{key}"
|
91
91
|
url = "https://s3.amazonaws.com/#{$conf.s3_bucket}/#{$conf.s3_key_prefix}/#{fname}"
|
92
92
|
ext = File.extname(fname)[1..-1]
|
93
|
-
|
94
|
-
|
93
|
+
data = {'url' => url, 's3' => true, 'shortened' => key,
|
94
|
+
'extension' => ext, 'set-count' => 1}
|
95
|
+
data = params.merge(data)
|
95
96
|
|
96
97
|
$redis.set(key, sha)
|
97
|
-
$redis.hmset(hash_key, *data)
|
98
|
+
$redis.hmset(hash_key, *arrayify_hash(data))
|
98
99
|
|
99
|
-
|
100
|
+
data
|
100
101
|
end
|
101
102
|
|
102
103
|
def shorten(url, options = {})
|
@@ -116,7 +117,7 @@ class Shortener
|
|
116
117
|
if (!prev_set['max-clicks'] && !prev_set['expire'] &&
|
117
118
|
(prev_set['url'] == url.to_s))
|
118
119
|
$redis.hincrby(check_key, 'set-count', 1)
|
119
|
-
return
|
120
|
+
return prev_set
|
120
121
|
end
|
121
122
|
end
|
122
123
|
|
@@ -136,19 +137,19 @@ class Shortener
|
|
136
137
|
sha = Digest::SHA1.hexdigest(url.to_s)
|
137
138
|
$redis.set(key, sha)
|
138
139
|
|
139
|
-
hsh_data =
|
140
|
-
hsh_data
|
140
|
+
hsh_data = {'shortened' => key, 'url' => url, 'set-count' => 1}
|
141
|
+
hsh_data['max-clicks'] = options['max-clicks'].to_i if options['max-clicks']
|
141
142
|
|
142
143
|
if options['expire'] # set expire time if specified
|
143
144
|
ttl = options['expire'].to_i
|
144
145
|
ttl_key = "expire:#{sha}:#{key}"
|
145
146
|
$redis.set(ttl_key, "#{sha}:#{key}")
|
146
147
|
$redis.expire(ttl_key, ttl)
|
147
|
-
hsh_data
|
148
|
+
hsh_data[:expire] = ttl_key
|
148
149
|
end
|
149
|
-
$redis.hmset("data:#{sha}:#{key}", *hsh_data)
|
150
|
+
$redis.hmset("data:#{sha}:#{key}", *arrayify_hash(hsh_data))
|
150
151
|
|
151
|
-
|
152
|
+
hsh_data
|
152
153
|
end
|
153
154
|
|
154
155
|
def check_cache(url)
|
@@ -158,7 +159,7 @@ class Shortener
|
|
158
159
|
short = $redis.hgetall(key)
|
159
160
|
unless short == {} || short['expire'] || short['max-clicks']
|
160
161
|
$redis.hincrby(key, 'set-count', 1)
|
161
|
-
return short
|
162
|
+
return short
|
162
163
|
end
|
163
164
|
end
|
164
165
|
nil
|
@@ -194,6 +195,10 @@ class Shortener
|
|
194
195
|
ret
|
195
196
|
end
|
196
197
|
|
198
|
+
def arrayify_hash(hsh)
|
199
|
+
hsh.keys.map {|k| [k, hsh[k]] }.flatten
|
200
|
+
end
|
201
|
+
|
197
202
|
end
|
198
203
|
|
199
204
|
before do
|
@@ -224,17 +229,16 @@ class Shortener
|
|
224
229
|
end
|
225
230
|
|
226
231
|
get '/delete/:id.:format' do |id, format|
|
227
|
-
puts "#{id}, #{format}"
|
228
232
|
delete_short(id)
|
229
233
|
if format == 'json'
|
230
234
|
content_type :json
|
231
|
-
{success: true,
|
235
|
+
{success: true, shortened: id}.to_json
|
232
236
|
else
|
233
237
|
redirect :index
|
234
238
|
end
|
235
239
|
end
|
236
240
|
|
237
|
-
get '/upload',
|
241
|
+
get '/upload', s3_available: true do
|
238
242
|
policy = $conf.s3_policy
|
239
243
|
signature = $conf.s3_signature(policy)
|
240
244
|
|
@@ -294,11 +298,12 @@ class Shortener
|
|
294
298
|
end
|
295
299
|
|
296
300
|
post '/upload.?:format?' do |format|
|
297
|
-
|
298
|
-
puts "set #{
|
301
|
+
@data = set_upload_short(params['shortener'])
|
302
|
+
puts "set #{@data['shortened']} to #{params['shortener']['file_name']}"
|
303
|
+
@url = "#{base_url}/#{@data['shortened']}"
|
299
304
|
if format == 'json'
|
300
305
|
content_type :json
|
301
|
-
{
|
306
|
+
@data.merge({html: haml(:display, layout: false)}).to_json
|
302
307
|
else
|
303
308
|
redirect :index
|
304
309
|
end
|
@@ -312,11 +317,12 @@ class Shortener
|
|
312
317
|
# essentally, params = params
|
313
318
|
end
|
314
319
|
|
315
|
-
@
|
320
|
+
@data = set_or_fetch_url(params["shortener"])
|
321
|
+
@url = "#{base_url}/#{@data['shortened']}"
|
316
322
|
puts "set #{@url} to #{params['shortener']['url']}"
|
317
323
|
if format == 'json'
|
318
324
|
content_type :json
|
319
|
-
{success: true,
|
325
|
+
@data.merge({success: true, html: haml(:display, :layout => false)}).to_json
|
320
326
|
else
|
321
327
|
haml :display
|
322
328
|
end
|
@@ -0,0 +1,147 @@
|
|
1
|
+
|
2
|
+
class Shortener
|
3
|
+
class Short
|
4
|
+
|
5
|
+
SHORT_KEYS = [:url, :shortened, :type, :ext, :s3, :'click-count', :'max-count',
|
6
|
+
:'set-count', :'expire-time', :sha]
|
7
|
+
|
8
|
+
attr_reader :data
|
9
|
+
|
10
|
+
module ClassMethods
|
11
|
+
|
12
|
+
# set a shortened url
|
13
|
+
def shorten(url, conf = nil)
|
14
|
+
opts = {'shortener' => {'url' => url}.to_json}
|
15
|
+
response = request(:post, :add, conf, opts)
|
16
|
+
if response.is_a?(Net::HTTPOK)
|
17
|
+
return Short.new(response.body, conf)
|
18
|
+
else
|
19
|
+
raise "OH SHIT! #{response}"
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
# get data for a short, including full url.
|
24
|
+
def fetch(short, conf = nil)
|
25
|
+
response = request(:get, :fetch, conf, short)
|
26
|
+
Short.new(response.body, conf)
|
27
|
+
end
|
28
|
+
|
29
|
+
# post a file to the configured s3 bucket and set a short.
|
30
|
+
def upload(file)
|
31
|
+
end
|
32
|
+
|
33
|
+
# fetch data on multiple shorts
|
34
|
+
def index(start = 0, stop = nil, conf = nil)
|
35
|
+
response = request(:get, :index, conf)
|
36
|
+
data = JSON.parse(response.body)
|
37
|
+
shorts = data.map {|sh| Short.new(sh, conf)}
|
38
|
+
shorts
|
39
|
+
end
|
40
|
+
|
41
|
+
# delete a short
|
42
|
+
def delete(short, conf = nil)
|
43
|
+
response = request(:get, :delete, conf, short)
|
44
|
+
Short.new(response.body, conf)
|
45
|
+
end
|
46
|
+
|
47
|
+
# build a request based on configurations
|
48
|
+
def request(type, end_point, conf = nil, args = nil)
|
49
|
+
config = conf || Shortener::Configuration.current
|
50
|
+
case type
|
51
|
+
when :post
|
52
|
+
Net::HTTP.post_form(config.uri_for(end_point), args)
|
53
|
+
when :get
|
54
|
+
Net::HTTP.get_response(config.uri_for(end_point, args))
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
end # => ClassMethods
|
59
|
+
|
60
|
+
class << self; include ClassMethods; end
|
61
|
+
|
62
|
+
# Set up this short. Will:
|
63
|
+
# * store the configuration if necessary
|
64
|
+
# * parse JSON if short is JSON
|
65
|
+
# * symbolize the shorts keys
|
66
|
+
# * turn string numbers in to actual numbers.
|
67
|
+
def initialize(short, conf = nil)
|
68
|
+
@configuration = conf unless conf.nil?
|
69
|
+
short = parse_return(short) unless short.is_a?(Hash)
|
70
|
+
@data = symbolize_keys(short)
|
71
|
+
normalize_data
|
72
|
+
end
|
73
|
+
|
74
|
+
# the configuration that this short will use.
|
75
|
+
def configuration
|
76
|
+
@configuation.nil? ? Shortener::Configuration.current : @configuration
|
77
|
+
end
|
78
|
+
|
79
|
+
# an alternative way to fetch stuff from @data
|
80
|
+
def [](key)
|
81
|
+
@data[key.to_sym]
|
82
|
+
end
|
83
|
+
|
84
|
+
# return a URI of the URL
|
85
|
+
def uri
|
86
|
+
URI.parse(url)
|
87
|
+
end
|
88
|
+
|
89
|
+
# shortened combined with config#shortener_url
|
90
|
+
def short_url
|
91
|
+
"#{configuration.shortener_url}/#{shortened}"
|
92
|
+
end
|
93
|
+
|
94
|
+
# a URI of short url.
|
95
|
+
def short_uri
|
96
|
+
URI.parse(short_url)
|
97
|
+
end
|
98
|
+
|
99
|
+
# allow the updating of a field. considering an arity like:
|
100
|
+
# update(hsh_of_fields_with_values)
|
101
|
+
# and it will create a POST request to send server side.
|
102
|
+
def update
|
103
|
+
#TODO
|
104
|
+
end
|
105
|
+
|
106
|
+
SHORT_KEYS.each do |key|
|
107
|
+
name = key.to_s.include?('-') ? key.to_s.gsub('-', '_').to_sym : key
|
108
|
+
define_method name do
|
109
|
+
@data[key]
|
110
|
+
end
|
111
|
+
end
|
112
|
+
|
113
|
+
def pretty_print
|
114
|
+
|
115
|
+
end
|
116
|
+
|
117
|
+
private
|
118
|
+
|
119
|
+
# used to turn the hash keys of the data hash in to symbols.
|
120
|
+
def symbolize_keys(hash)
|
121
|
+
ret = Hash.new
|
122
|
+
hash.each do |k,v|
|
123
|
+
ret[k.to_sym] = v
|
124
|
+
end
|
125
|
+
ret
|
126
|
+
end
|
127
|
+
|
128
|
+
# turn string numbers in to actual numbers.
|
129
|
+
def normalize_data
|
130
|
+
[:'click-count', :'max-count', :'set-count'].each do |k|
|
131
|
+
@data[k] = @data[k].to_i
|
132
|
+
end
|
133
|
+
@data[:'click-count'] ||= 0
|
134
|
+
end
|
135
|
+
|
136
|
+
# parse JSON safely.
|
137
|
+
def parse_return(json)
|
138
|
+
begin
|
139
|
+
return JSON.parse(json)
|
140
|
+
rescue Exception => boom
|
141
|
+
raise "OH SHIT! #{boom}\n\n #{json}"
|
142
|
+
end
|
143
|
+
end
|
144
|
+
|
145
|
+
end
|
146
|
+
end
|
147
|
+
require_relative '../shortener'
|
data/lib/shortener/version.rb
CHANGED
data/lib/shortener.rb
CHANGED
@@ -1,9 +1,12 @@
|
|
1
|
+
require 'uri'
|
2
|
+
require 'json'
|
3
|
+
require 'net/http'
|
1
4
|
dir = File.expand_path(File.dirname(__FILE__))
|
2
5
|
|
3
6
|
require File.join(dir, 'shortener', 'version')
|
4
|
-
require File.join(dir, 'shortener', 'client')
|
5
7
|
require File.join(dir, 'shortener', 'configuration')
|
8
|
+
require File.join(dir, 'shortener', 'short')
|
6
9
|
|
7
10
|
class Shortener
|
8
|
-
|
11
|
+
class << self; include Shortener::Short::ClassMethods; end
|
9
12
|
end
|
data/tasks/heroku.rake
CHANGED
@@ -22,7 +22,7 @@ namespace :heroku do
|
|
22
22
|
require_relative '../lib/shortener'
|
23
23
|
$name = ENV['APPNAME'] || "shner-#{`whoami`.chomp}"
|
24
24
|
cmd = Dir.pwd =~ /heroku$/ ? "" : "cd heroku && "
|
25
|
-
cmd += "heroku create #{name}"
|
25
|
+
cmd += "heroku create #{$name}"
|
26
26
|
cmd += " && heroku addons:add redistogo:nano"
|
27
27
|
cmd += " && heroku config:add #{Shortener::Configuration.new.to_params}"
|
28
28
|
cmd += " && heroku addons:add custom_domains:basic"
|
data/test/test_short.rb
ADDED
@@ -0,0 +1,68 @@
|
|
1
|
+
require 'minitest/autorun'
|
2
|
+
require_relative '../lib/shortener/short'
|
3
|
+
|
4
|
+
class TestShortenerShort < MiniTest::Unit::TestCase
|
5
|
+
|
6
|
+
def setup
|
7
|
+
@short = Shortener::Short.new('url' => 'http://google.com', 'shortened' => '12345',
|
8
|
+
'set-count' => '12')
|
9
|
+
end
|
10
|
+
|
11
|
+
def test_instance_methods
|
12
|
+
assert @short.shortened == '12345'
|
13
|
+
assert @short.url == 'http://google.com'
|
14
|
+
assert @short.set_count == 12
|
15
|
+
end
|
16
|
+
|
17
|
+
def test_brackets
|
18
|
+
assert @short['shortened'] == '12345'
|
19
|
+
assert @short[:shortened] == '12345'
|
20
|
+
assert @short['set-count'] == 12
|
21
|
+
end
|
22
|
+
|
23
|
+
def test_defaults_ensure_keys
|
24
|
+
short = Shortener.shorten('www.google.com')
|
25
|
+
assert !short.shortened.nil?
|
26
|
+
assert !short.url.nil?
|
27
|
+
assert !short.set_count.nil?
|
28
|
+
assert !short.click_count.nil?
|
29
|
+
end
|
30
|
+
|
31
|
+
def test_short_url
|
32
|
+
assert @short.short_url == "#{@short.configuration.shortener_url}/#{@short.shortened}"
|
33
|
+
end
|
34
|
+
|
35
|
+
def test_uri
|
36
|
+
assert @short.uri == URI.parse(@short.url)
|
37
|
+
end
|
38
|
+
|
39
|
+
def test_add
|
40
|
+
short = Shortener::Short.shorten('www.google.com')
|
41
|
+
assert short.is_a?(Shortener::Short)
|
42
|
+
assert short.shortened.nil? == false
|
43
|
+
assert short.url == 'http://www.google.com'
|
44
|
+
assert short['success']
|
45
|
+
end
|
46
|
+
|
47
|
+
def test_index
|
48
|
+
ind = Shortener::Short.index
|
49
|
+
assert ind.is_a?(Array)
|
50
|
+
assert ind.length >= 1
|
51
|
+
assert ind.first.is_a?(Shortener::Short)
|
52
|
+
end
|
53
|
+
|
54
|
+
def test_fetch
|
55
|
+
short = Shortener.shorten('www.google.com')
|
56
|
+
short2 = Shortener::Short.fetch(short.shortened)
|
57
|
+
assert short.shortened == short2.shortened
|
58
|
+
short = Shortener.fetch('nope')
|
59
|
+
assert short[:success] == false
|
60
|
+
end
|
61
|
+
|
62
|
+
def test_delete
|
63
|
+
add = Shortener.shorten('www.google.com')
|
64
|
+
del = Shortener.delete(add['shortened'])
|
65
|
+
assert del['success']
|
66
|
+
end
|
67
|
+
|
68
|
+
end
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: short
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.4.0
|
5
5
|
prerelease:
|
6
6
|
platform: ruby
|
7
7
|
authors:
|
@@ -9,11 +9,11 @@ authors:
|
|
9
9
|
autorequire:
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
|
-
date:
|
12
|
+
date: 2012-01-04 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: sinatra
|
16
|
-
requirement: &
|
16
|
+
requirement: &70301329586160 !ruby/object:Gem::Requirement
|
17
17
|
none: false
|
18
18
|
requirements:
|
19
19
|
- - ! '>='
|
@@ -21,10 +21,10 @@ dependencies:
|
|
21
21
|
version: '0'
|
22
22
|
type: :development
|
23
23
|
prerelease: false
|
24
|
-
version_requirements: *
|
24
|
+
version_requirements: *70301329586160
|
25
25
|
- !ruby/object:Gem::Dependency
|
26
26
|
name: redis-namespace
|
27
|
-
requirement: &
|
27
|
+
requirement: &70301329585240 !ruby/object:Gem::Requirement
|
28
28
|
none: false
|
29
29
|
requirements:
|
30
30
|
- - ! '>='
|
@@ -32,10 +32,10 @@ dependencies:
|
|
32
32
|
version: '0'
|
33
33
|
type: :development
|
34
34
|
prerelease: false
|
35
|
-
version_requirements: *
|
35
|
+
version_requirements: *70301329585240
|
36
36
|
- !ruby/object:Gem::Dependency
|
37
37
|
name: haml
|
38
|
-
requirement: &
|
38
|
+
requirement: &70301329583820 !ruby/object:Gem::Requirement
|
39
39
|
none: false
|
40
40
|
requirements:
|
41
41
|
- - ! '>='
|
@@ -43,10 +43,10 @@ dependencies:
|
|
43
43
|
version: '0'
|
44
44
|
type: :development
|
45
45
|
prerelease: false
|
46
|
-
version_requirements: *
|
46
|
+
version_requirements: *70301329583820
|
47
47
|
- !ruby/object:Gem::Dependency
|
48
48
|
name: turn
|
49
|
-
requirement: &
|
49
|
+
requirement: &70301329582280 !ruby/object:Gem::Requirement
|
50
50
|
none: false
|
51
51
|
requirements:
|
52
52
|
- - ! '>='
|
@@ -54,7 +54,7 @@ dependencies:
|
|
54
54
|
version: '0'
|
55
55
|
type: :development
|
56
56
|
prerelease: false
|
57
|
-
version_requirements: *
|
57
|
+
version_requirements: *70301329582280
|
58
58
|
description: A (hopefully) easy and handy deployable APIable way to shorten links.
|
59
59
|
email:
|
60
60
|
- jake@jakewilkins.com
|
@@ -64,12 +64,12 @@ extensions: []
|
|
64
64
|
extra_rdoc_files: []
|
65
65
|
files:
|
66
66
|
- .gitignore
|
67
|
+
- Guardfile
|
67
68
|
- README.md
|
68
69
|
- Rakefile
|
69
70
|
- bin/short
|
70
71
|
- config.ru
|
71
72
|
- lib/shortener.rb
|
72
|
-
- lib/shortener/client.rb
|
73
73
|
- lib/shortener/configuration.rb
|
74
74
|
- lib/shortener/server.rb
|
75
75
|
- lib/shortener/server/Gemfile
|
@@ -100,12 +100,13 @@ files:
|
|
100
100
|
- lib/shortener/server/views/s3/layout.haml
|
101
101
|
- lib/shortener/server/views/s3/video.haml
|
102
102
|
- lib/shortener/server/views/upload.haml
|
103
|
+
- lib/shortener/short.rb
|
103
104
|
- lib/shortener/version.rb
|
104
105
|
- short.gemspec
|
105
106
|
- tasks/heroku.rake
|
106
|
-
- test/test_client.rb
|
107
107
|
- test/test_configuration.rb
|
108
108
|
- test/test_server.rb
|
109
|
+
- test/test_short.rb
|
109
110
|
homepage: ''
|
110
111
|
licenses: []
|
111
112
|
post_install_message:
|
@@ -131,6 +132,6 @@ signing_key:
|
|
131
132
|
specification_version: 3
|
132
133
|
summary: A Link Shortener
|
133
134
|
test_files:
|
134
|
-
- test/test_client.rb
|
135
135
|
- test/test_configuration.rb
|
136
136
|
- test/test_server.rb
|
137
|
+
- test/test_short.rb
|
data/lib/shortener/client.rb
DELETED
@@ -1,69 +0,0 @@
|
|
1
|
-
dir = File.expand_path(File.dirname(__FILE__))
|
2
|
-
require 'net/http'
|
3
|
-
require 'json'
|
4
|
-
require File.join(dir, 'configuration')
|
5
|
-
|
6
|
-
class Shortener
|
7
|
-
class Client
|
8
|
-
|
9
|
-
attr_accessor :configuration
|
10
|
-
|
11
|
-
def initialize(options = Hash.new)
|
12
|
-
@configuration = Configuration.new(options)
|
13
|
-
end
|
14
|
-
|
15
|
-
# set a shortened url
|
16
|
-
def shorten(url)
|
17
|
-
opts = {'shortener' => {'url' => url}.to_json}
|
18
|
-
response= request(:post, :add, opts)
|
19
|
-
if response.is_a?(Net::HTTPOK)
|
20
|
-
return parse_return(response.body)
|
21
|
-
else
|
22
|
-
raise "OH SHIT! #{response}"
|
23
|
-
end
|
24
|
-
end
|
25
|
-
|
26
|
-
# get data for a short, including full url.
|
27
|
-
def fetch(short)
|
28
|
-
response = request(:get, :fetch, short)
|
29
|
-
parse_return(response)
|
30
|
-
end
|
31
|
-
|
32
|
-
# post a file to the configured s3 bucket and set a short.
|
33
|
-
def upload(file)
|
34
|
-
end
|
35
|
-
|
36
|
-
# fetch data on multiple shorts
|
37
|
-
def index(start = 0, stop = nil)
|
38
|
-
response = request(:get, :index)
|
39
|
-
parse_return(response)
|
40
|
-
end
|
41
|
-
|
42
|
-
# delete a short
|
43
|
-
def delete(short)
|
44
|
-
response = request(:get, :delete, short)
|
45
|
-
parse_return(response)
|
46
|
-
end
|
47
|
-
|
48
|
-
private
|
49
|
-
|
50
|
-
# build a request based on configurations
|
51
|
-
def request(type, end_point, args = nil)
|
52
|
-
case type
|
53
|
-
when :post
|
54
|
-
Net::HTTP.post_form(@configuration.uri_for(end_point), args)
|
55
|
-
when :get
|
56
|
-
Net::HTTP.get(@configuration.uri_for(end_point, args))
|
57
|
-
end
|
58
|
-
end
|
59
|
-
|
60
|
-
def parse_return(json)
|
61
|
-
begin
|
62
|
-
return JSON.parse(json)
|
63
|
-
rescue Exception => boom
|
64
|
-
raise "OH SHIT! #{boom}\n\n #{json}"
|
65
|
-
end
|
66
|
-
end
|
67
|
-
|
68
|
-
end # => Client
|
69
|
-
end # => Shortener
|
data/test/test_client.rb
DELETED
@@ -1,26 +0,0 @@
|
|
1
|
-
require 'minitest/autorun'
|
2
|
-
require_relative '../lib/shortener/client'
|
3
|
-
|
4
|
-
class TestShortenerClient < MiniTest::Unit::TestCase
|
5
|
-
|
6
|
-
def setup
|
7
|
-
@client = Shortener::Client.new
|
8
|
-
end
|
9
|
-
|
10
|
-
def test_add
|
11
|
-
short = @client.shorten('www.google.com')
|
12
|
-
assert short['success']
|
13
|
-
end
|
14
|
-
|
15
|
-
def test_index
|
16
|
-
ind = @client.index
|
17
|
-
assert ind.is_a?(Array)
|
18
|
-
end
|
19
|
-
|
20
|
-
def test_delete
|
21
|
-
add = @client.shorten('www.google.com')
|
22
|
-
del = @client.delete(add['short'])
|
23
|
-
assert del['success']
|
24
|
-
end
|
25
|
-
|
26
|
-
end
|