shortener 0.0.2 → 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +3 -24
- data/README.rdoc +40 -11
- data/Rakefile +7 -21
- data/app/controllers/shortener/shortened_urls_controller.rb +22 -23
- data/app/helpers/shortener/shortener_helper.rb +6 -21
- data/app/models/shortener/shortened_url.rb +66 -74
- data/lib/generators/shortener/templates/migration.rb +17 -14
- data/lib/shortener.rb +22 -11
- data/lib/shortener/active_record_extension.rb +5 -0
- data/lib/shortener/engine.rb +3 -7
- data/lib/shortener/railtie.rb +10 -0
- data/lib/shortener/version.rb +2 -2
- data/shortener.gemspec +7 -5
- data/spec/controllers/shortened_urls_controller_spec.rb +19 -0
- data/spec/dummy/.gitignore +1 -0
- data/{test → spec}/dummy/Rakefile +0 -0
- data/{test → spec}/dummy/app/controllers/application_controller.rb +0 -0
- data/{test → spec}/dummy/app/helpers/application_helper.rb +0 -0
- data/spec/dummy/app/models/user.rb +3 -0
- data/{test → spec}/dummy/app/views/layouts/application.html.erb +0 -0
- data/{test → spec}/dummy/config.ru +0 -0
- data/{test → spec}/dummy/config/application.rb +0 -1
- data/{test → spec}/dummy/config/boot.rb +0 -0
- data/{test → spec}/dummy/config/database.yml +0 -0
- data/{test → spec}/dummy/config/environment.rb +0 -0
- data/{test → spec}/dummy/config/environments/development.rb +0 -3
- data/{test → spec}/dummy/config/environments/test.rb +0 -5
- data/{test → spec}/dummy/config/initializers/backtrace_silencers.rb +0 -0
- data/{test → spec}/dummy/config/initializers/inflections.rb +0 -0
- data/{test → spec}/dummy/config/initializers/mime_types.rb +0 -0
- data/{test → spec}/dummy/config/initializers/secret_token.rb +0 -0
- data/{test → spec}/dummy/config/initializers/session_store.rb +0 -0
- data/{test → spec}/dummy/config/locales/en.yml +0 -0
- data/spec/dummy/config/routes.rb +4 -0
- data/{test/dummy/public/stylesheets → spec/dummy/db}/.gitkeep +0 -0
- data/spec/dummy/db/migrate/20120213084304_create_shortened_urls_table.rb +25 -0
- data/spec/dummy/db/migrate/20120214023758_create_users.rb +8 -0
- data/spec/dummy/db/schema.rb +34 -0
- data/{test → spec}/dummy/public/favicon.ico +0 -0
- data/{test → spec}/dummy/script/rails +0 -0
- data/spec/helpers/shortener_helper_spec.rb +12 -0
- data/spec/models/shortened_url_spec.rb +68 -0
- data/spec/spec_helper.rb +12 -0
- metadata +71 -49
- data/test/dummy/config/environments/production.rb +0 -49
- data/test/dummy/config/routes.rb +0 -58
- data/test/dummy/public/404.html +0 -26
- data/test/dummy/public/422.html +0 -26
- data/test/dummy/public/500.html +0 -26
- data/test/dummy/public/javascripts/application.js +0 -2
- data/test/dummy/public/javascripts/controls.js +0 -965
- data/test/dummy/public/javascripts/dragdrop.js +0 -974
- data/test/dummy/public/javascripts/effects.js +0 -1123
- data/test/dummy/public/javascripts/prototype.js +0 -6001
- data/test/dummy/public/javascripts/rails.js +0 -191
- data/test/integration/navigation_test.rb +0 -7
- data/test/shortener_test.rb +0 -7
- data/test/support/integration_case.rb +0 -5
- data/test/test_helper.rb +0 -22
data/.gitignore
CHANGED
@@ -2,33 +2,12 @@
|
|
2
2
|
log/*
|
3
3
|
tmp/**/*
|
4
4
|
bin/*
|
5
|
-
|
6
|
-
config/database.yml
|
7
|
-
db/*.sqlite3
|
8
|
-
*~
|
9
|
-
public/photos/*
|
10
|
-
\#*\#
|
11
|
-
#Ignore all log files and process ID files
|
12
5
|
*.log
|
13
|
-
|
14
|
-
|
15
|
-
#ignore all generated pshinx config files
|
16
|
-
*.sphinx.conf
|
17
|
-
|
18
|
-
#ignore all sphinx DB files
|
19
|
-
*.spa
|
20
|
-
*.spd
|
21
|
-
*.sph
|
22
|
-
*.spi
|
23
|
-
*.spk
|
24
|
-
*.spl
|
25
|
-
*.spm
|
26
|
-
*.spp
|
27
|
-
|
28
|
-
#ignore radrails files and temp files
|
6
|
+
*~
|
29
7
|
.project
|
30
8
|
.loadpath
|
31
9
|
._*
|
32
|
-
|
33
10
|
.bundle
|
34
11
|
*.gem
|
12
|
+
*.swp
|
13
|
+
Gemfile.lock
|
data/README.rdoc
CHANGED
@@ -1,29 +1,32 @@
|
|
1
1
|
= Shortener
|
2
2
|
|
3
|
-
Shortener makes it easy to create shortened URLs
|
3
|
+
Shortener is a Rails Engine Gem that makes it easy to create and interpret shortened URLs on your own domain from within your Rails application. Once installed Shortener will generate, store URLS and "unshorten" shortened URLs for your applications visitors, all whilst collecting basic usage metrics.
|
4
|
+
|
5
|
+
---
|
4
6
|
|
5
7
|
== Overview
|
6
8
|
|
7
|
-
The majority of the
|
9
|
+
The majority of the Shortener consists of three parts:
|
8
10
|
|
9
|
-
* a model for storing the details of the shortened link
|
10
|
-
* a controller to accept incoming requests
|
11
|
+
* a model for storing the details of the shortened link;
|
12
|
+
* a controller to accept incoming requests and redirecting them to the target URL;
|
13
|
+
* a helper for generating shortened URLs from controllers and views.
|
11
14
|
|
12
|
-
=== Some niceities of
|
15
|
+
=== Some niceities of Shortener:
|
13
16
|
|
14
17
|
* The controller does a 301 redirect, which is the recommended type of redirect for maintaining maximum google juice to the original URL;
|
15
|
-
* A unique code of
|
18
|
+
* A unique alphanumeric code of generated for each shortened link, this means that we can get more unique combinations than if we just used numbers;
|
16
19
|
* The link records a count of how many times it has been “un-shortened”;
|
17
|
-
* The link can be
|
20
|
+
* The link can be associated with a user, this allows for stats of the link usage for a particular user and other interesting things;
|
18
21
|
* The controller spawns a new thread to record information to the database, allowing the redirect to happen as quickly as possible;
|
19
22
|
|
20
|
-
=== Future
|
23
|
+
=== Future improvements:
|
21
24
|
|
22
25
|
* There has not been an attempt to remove ambiguous characters (i.e. 1 l and capital i, or 0 and O etc.) from the unique key generated for the link. This means people might copy the link incorrectly if copying the link by hand;
|
23
26
|
* The shortened links are found with a case-insensitive search on the unique key. This means that the system can’t take advantage of upper and lower case to increase the number of unique combinations. This may have an effect for people copying the link by hand;
|
24
27
|
* The system could pre-generate unique keys in advance, avoiding the database penalty when checking that a newly generated key is unique;
|
25
28
|
* The system could store the shortened URL if the url is to be continually rendered;
|
26
|
-
* Some implementations might want duplicate links to be generated each time a user
|
29
|
+
* Some implementations might want duplicate links to be generated each time a user request a shortened link.
|
27
30
|
|
28
31
|
|
29
32
|
== Installation
|
@@ -38,6 +41,10 @@ After you install Shortener run the generator:
|
|
38
41
|
|
39
42
|
This generator will create a migration to create the shortened_urls table where your shortened URLs will be stored.
|
40
43
|
|
44
|
+
Then add to your routes:
|
45
|
+
|
46
|
+
match '/:id' => "shortener/shortened_urls#show"
|
47
|
+
|
41
48
|
== Usage
|
42
49
|
|
43
50
|
To generate a Shortened URL object for the URL "http://dealush.com" within your controller / models do the following:
|
@@ -54,6 +61,28 @@ To generate and display a shortened URL in your application use the helper metho
|
|
54
61
|
|
55
62
|
This will generate a shortened URL. store it to the db and return a string representing the shortened URL.
|
56
63
|
|
57
|
-
|
64
|
+
=== Shortened URLs with owner
|
65
|
+
|
66
|
+
You can link shortened URLs to an owner, to scope them. To do so, add the following line to the models which will act as owners:
|
67
|
+
|
68
|
+
class User < ActiveRecord::Base
|
69
|
+
has_shortened_urls
|
70
|
+
end
|
71
|
+
|
72
|
+
This will allow you to pass the owner when generating URLs:
|
73
|
+
|
74
|
+
Shortener::ShortenedURL.generate("dealush.com", user)
|
75
|
+
|
76
|
+
And to access those URLs:
|
77
|
+
|
78
|
+
user.shortened_urls
|
79
|
+
|
80
|
+
== Origins
|
81
|
+
|
82
|
+
Shortener is based on code from Dealush[http://dealush.com], for a bit of backstory to Shortener see this {blog post}[http://jamespmcgrath.com/a-simple-link-shortener-in-rails/].
|
83
|
+
|
84
|
+
== Authors
|
85
|
+
|
86
|
+
* {James McGrath}[https://github.com/jpmcgrath]
|
87
|
+
* {Michael Reinsch}[https://github.com/mreinsch]
|
58
88
|
|
59
|
-
This is the first release and still has some bugs. I will be releasing fixes for these bugs soon.
|
data/Rakefile
CHANGED
@@ -1,26 +1,12 @@
|
|
1
|
-
|
2
|
-
require '
|
3
|
-
|
4
|
-
|
5
|
-
rescue LoadError
|
6
|
-
puts 'You must `gem install bundler` and `bundle install` to run rake tasks'
|
7
|
-
end
|
8
|
-
|
9
|
-
require 'rake'
|
10
|
-
require 'rake/rdoctask'
|
11
|
-
|
12
|
-
require 'rake/testtask'
|
13
|
-
|
14
|
-
Rake::TestTask.new(:test) do |t|
|
15
|
-
t.libs << 'lib'
|
16
|
-
t.libs << 'test'
|
17
|
-
t.pattern = 'test/**/*_test.rb'
|
18
|
-
t.verbose = false
|
19
|
-
end
|
1
|
+
require 'bundler'
|
2
|
+
require 'rspec/core/rake_task'
|
3
|
+
require 'rdoc/task'
|
4
|
+
Bundler::GemHelper.install_tasks
|
20
5
|
|
21
|
-
task :default => :
|
6
|
+
task :default => :spec
|
7
|
+
RSpec::Core::RakeTask.new
|
22
8
|
|
23
|
-
|
9
|
+
RDoc::Task.new :rdoc do |rdoc|
|
24
10
|
rdoc.rdoc_dir = 'rdoc'
|
25
11
|
rdoc.title = 'Shortener'
|
26
12
|
rdoc.options << '--line-numbers' << '--inline-source'
|
@@ -1,27 +1,26 @@
|
|
1
|
-
|
2
|
-
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
end
|
17
|
-
# do a 301 redirect to the destination url
|
18
|
-
head :moved_permanently, :location => sl.url
|
19
|
-
else
|
20
|
-
# if we don't find the shortened link, redirect to the root
|
21
|
-
# make this configurable in future versions
|
22
|
-
head :moved_permanently, :location => root_url
|
1
|
+
class Shortener::ShortenedUrlsController < ::ApplicationController
|
2
|
+
|
3
|
+
# find the real link for the shortened link key and redirect
|
4
|
+
def show
|
5
|
+
# pull the link out of the db
|
6
|
+
sl = ::Shortener::ShortenedUrl.find_by_unique_key(params[:id])
|
7
|
+
|
8
|
+
if sl
|
9
|
+
# don't want to wait for the increment to happen, make it snappy!
|
10
|
+
# this is the place to enhance the metrics captured
|
11
|
+
# for the system. You could log the request origin
|
12
|
+
# browser type, ip address etc.
|
13
|
+
Thread.new do
|
14
|
+
sl.increment!(:use_count)
|
15
|
+
ActiveRecord::Base.connection.close
|
23
16
|
end
|
17
|
+
# do a 301 redirect to the destination url
|
18
|
+
redirect_to sl.url, :status => :moved_permanently
|
19
|
+
else
|
20
|
+
# if we don't find the shortened link, redirect to the root
|
21
|
+
# make this configurable in future versions
|
22
|
+
redirect_to '/'
|
24
23
|
end
|
25
|
-
|
26
24
|
end
|
25
|
+
|
27
26
|
end
|
@@ -1,24 +1,9 @@
|
|
1
1
|
module Shortener::ShortenerHelper
|
2
|
-
|
3
|
-
# generate a url from
|
4
|
-
def
|
5
|
-
|
6
|
-
short_url
|
7
|
-
|
8
|
-
if url_object.class != String #== ShortenedUrl
|
9
|
-
if user.nil?
|
10
|
-
short_url = url_object
|
11
|
-
else
|
12
|
-
# if the user has passed in a shortened url, with a user, then
|
13
|
-
# work out the link for the shortened url and make another with the
|
14
|
-
# passed user
|
15
|
-
short_url = Shortener::ShortenedUrl.generate(shortened_url(url_object), user)
|
16
|
-
end
|
17
|
-
else
|
18
|
-
short_url = Shortener::ShortenedUrl.generate(url_object, user)
|
19
|
-
end
|
20
|
-
|
21
|
-
return short_url.nil? ? nil : shortener_translate_url(short_url.unique_key)
|
2
|
+
|
3
|
+
# generate a url from a url string
|
4
|
+
def short_url(url, owner=nil)
|
5
|
+
short_url = Shortener::ShortenedUrl.generate(url, owner)
|
6
|
+
short_url ? url_for(:controller => :"shortener/shortened_urls", :action => :show, :id => short_url.unique_key, :only_path => false) : url
|
22
7
|
end
|
23
8
|
|
24
|
-
end
|
9
|
+
end
|
@@ -1,80 +1,72 @@
|
|
1
|
-
|
2
|
-
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
def init_unique_key
|
28
|
-
# generate a unique key for the link
|
29
|
-
begin
|
30
|
-
# has about 50 million possible combos
|
31
|
-
self.unique_key = ShortenedUrl::generate_unique_key
|
32
|
-
end while ShortenedUrl::find_by_unique_key self.unique_key
|
1
|
+
class Shortener::ShortenedUrl < ActiveRecord::Base
|
2
|
+
|
3
|
+
URL_PROTOCOL_HTTP = "http://"
|
4
|
+
REGEX_LINK_HAS_PROTOCOL = Regexp.new('\Ahttp:\/\/|\Ahttps:\/\/', Regexp::IGNORECASE)
|
5
|
+
|
6
|
+
validates :url, :presence => true
|
7
|
+
|
8
|
+
# allows the shortened link to be associated with a user
|
9
|
+
belongs_to :owner, :polymorphic => true
|
10
|
+
|
11
|
+
# ensure the url starts with it protocol and is normalized
|
12
|
+
def self.clean_url(url)
|
13
|
+
return nil if url.blank?
|
14
|
+
url = URL_PROTOCOL_HTTP + url.strip unless url =~ REGEX_LINK_HAS_PROTOCOL
|
15
|
+
URI.parse(url).normalize.to_s
|
16
|
+
end
|
17
|
+
|
18
|
+
# generate a shortened link from a url
|
19
|
+
# link to a user if one specified
|
20
|
+
# throw an exception if anything goes wrong
|
21
|
+
def self.generate!(orig_url, owner=nil)
|
22
|
+
# if we get a shortened_url object with a different owner, generate
|
23
|
+
# new one for the new owner. Otherwise return same object
|
24
|
+
if orig_url.is_a?(Shortener::ShortenedUrl)
|
25
|
+
return orig_url.owner == owner ? orig_url : generate!(orig_url.url, owner)
|
33
26
|
end
|
34
|
-
|
35
|
-
# generate
|
36
|
-
#
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
# return the url
|
50
|
-
return sl
|
27
|
+
|
28
|
+
# don't want to generate the link if it has already been generated
|
29
|
+
# so check the datastore
|
30
|
+
cleaned_url = clean_url(orig_url)
|
31
|
+
scope = owner ? owner.shortened_urls : self
|
32
|
+
scope.find_or_create_by_url(cleaned_url)
|
33
|
+
end
|
34
|
+
|
35
|
+
# return shortened url on success, nil on failure
|
36
|
+
def self.generate(orig_url, owner=nil)
|
37
|
+
begin
|
38
|
+
generate!(orig_url, owner)
|
39
|
+
rescue
|
40
|
+
nil
|
51
41
|
end
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
42
|
+
end
|
43
|
+
|
44
|
+
private
|
45
|
+
|
46
|
+
# we'll rely on the DB to make sure the unique key is really unique.
|
47
|
+
# if it isn't unique, the unique index will catch this and raise an error
|
48
|
+
def create
|
49
|
+
count = 0
|
50
|
+
begin
|
51
|
+
self.unique_key = generate_unique_key
|
52
|
+
super
|
53
|
+
rescue ActiveRecord::RecordNotUnique, ActiveRecord::StatementInvalid => err
|
54
|
+
if (count +=1) < 5
|
55
|
+
logger.info("retrying with different unique key")
|
56
|
+
retry
|
57
|
+
else
|
58
|
+
logger.info("too many retries, giving up")
|
59
|
+
raise
|
62
60
|
end
|
63
|
-
|
64
|
-
return sl
|
65
|
-
end
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
private
|
70
|
-
|
71
|
-
# generate a random string
|
72
|
-
# future mod to allow specifying a more expansive charst, like utf-8 chinese
|
73
|
-
def self.generate_unique_key(size = UNIQUE_KEY_LENGTH)
|
74
|
-
# not doing uppercase as url is case insensitive
|
75
|
-
charset = ('a'..'z').to_a + (0..9).to_a
|
76
|
-
(0...size).map{ charset.to_a[rand(charset.size)] }.join
|
77
61
|
end
|
78
|
-
|
79
62
|
end
|
63
|
+
|
64
|
+
# generate a random string
|
65
|
+
# future mod to allow specifying a more expansive charst, like utf-8 chinese
|
66
|
+
def generate_unique_key
|
67
|
+
# not doing uppercase as url is case insensitive
|
68
|
+
charset = ::Shortener.key_chars
|
69
|
+
(0...::Shortener.unique_key_length).map{ charset[rand(charset.size)] }.join
|
70
|
+
end
|
71
|
+
|
80
72
|
end
|
@@ -1,22 +1,25 @@
|
|
1
1
|
class CreateShortenedUrlsTable < ActiveRecord::Migration
|
2
|
-
def
|
2
|
+
def change
|
3
3
|
create_table :shortened_urls do |t|
|
4
|
-
|
5
|
-
t.integer :
|
6
|
-
t.string :
|
7
|
-
|
8
|
-
|
4
|
+
# we can link this to a user for interesting things
|
5
|
+
t.integer :owner_id
|
6
|
+
t.string :owner_type, :limit => 20
|
7
|
+
|
8
|
+
# the real url that we will redirect to
|
9
|
+
t.string :url, :null => false
|
10
|
+
|
11
|
+
# the unique key
|
12
|
+
t.string :unique_key, :limit => 10, :null => false
|
13
|
+
|
14
|
+
# how many times the link has been clicked
|
15
|
+
t.integer :use_count, :null => false, :default => 0
|
9
16
|
|
10
17
|
t.timestamps
|
11
18
|
end
|
12
|
-
|
13
|
-
add_index :shortened_urls, :unique_key # we will lookup the links in the db with this
|
14
|
-
add_index :shortened_urls, :user_id # and this
|
15
|
-
end
|
16
19
|
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
20
|
+
# we will lookup the links in the db by key and owners.
|
21
|
+
# also make sure the unique keys are actually unique
|
22
|
+
add_index :shortened_urls, :unique_key, :unique => true
|
23
|
+
add_index :shortened_urls, [:owner_id, :owner_type]
|
21
24
|
end
|
22
25
|
end
|
data/lib/shortener.rb
CHANGED
@@ -1,17 +1,28 @@
|
|
1
1
|
require "active_support/dependencies"
|
2
2
|
|
3
3
|
module Shortener
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
4
|
+
|
5
|
+
autoload :ActiveRecordExtension, "shortener/active_record_extension"
|
6
|
+
|
7
|
+
CHARSETS = {
|
8
|
+
:alphanum => ('a'..'z').to_a + (0..9).to_a,
|
9
|
+
:alphanumcase => ('a'..'z').to_a + ('A'..'Z').to_a + (0..9).to_a }
|
10
|
+
|
11
|
+
# default key length: 5 characters
|
12
|
+
mattr_accessor :unique_key_length
|
13
|
+
self.unique_key_length = 5
|
14
|
+
|
15
|
+
# character set to chose from:
|
16
|
+
# :alphanum - a-z0-9 - has about 60 million possible combos
|
17
|
+
# :alphanumcase - a-zA-Z0-9 - has about 900 million possible combos
|
18
|
+
mattr_accessor :charset
|
19
|
+
self.charset = :alphanum
|
20
|
+
|
21
|
+
def self.key_chars
|
22
|
+
CHARSETS[charset]
|
12
23
|
end
|
13
|
-
|
14
24
|
end
|
15
25
|
|
16
|
-
# Require our engine
|
17
|
-
require "shortener/
|
26
|
+
# Require our railtie and engine
|
27
|
+
require "shortener/railtie"
|
28
|
+
require "shortener/engine"
|