corpshort 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +11 -0
- data/.rspec +3 -0
- data/.travis.yml +5 -0
- data/Gemfile +8 -0
- data/Gemfile.lock +83 -0
- data/LICENSE.txt +21 -0
- data/README.md +59 -0
- data/Rakefile +6 -0
- data/app/public/style.css +320 -0
- data/app/views/edit.erb +26 -0
- data/app/views/index.erb +24 -0
- data/app/views/layout.erb +40 -0
- data/app/views/list.erb +11 -0
- data/app/views/show.erb +91 -0
- data/bin/console +14 -0
- data/bin/setup +8 -0
- data/config.ru +40 -0
- data/corpshort.gemspec +39 -0
- data/lib/corpshort.rb +7 -0
- data/lib/corpshort/app.rb +349 -0
- data/lib/corpshort/backends/base.rb +34 -0
- data/lib/corpshort/backends/dynamodb.rb +114 -0
- data/lib/corpshort/backends/memory.rb +61 -0
- data/lib/corpshort/backends/redis.rb +121 -0
- data/lib/corpshort/horizontal_pdf.rb +105 -0
- data/lib/corpshort/link.rb +83 -0
- data/lib/corpshort/version.rb +3 -0
- data/lib/corpshort/vertical_pdf.rb +107 -0
- metadata +240 -0
data/app/views/edit.erb
ADDED
@@ -0,0 +1,26 @@
|
|
1
|
+
<section>
|
2
|
+
<h2>Edit: <%= @link.name %></h2>
|
3
|
+
|
4
|
+
<form action='<%= update_path(@link) %>' method='POST'>
|
5
|
+
<input type='hidden' name='_method' value='PUT'>
|
6
|
+
<div class='form-row'>
|
7
|
+
<label for='edit_new_name'>Name</label>
|
8
|
+
<input type='text' id='edit_new_name' name='new_name' placeholder='short-name' value="<%= @link.name %>">
|
9
|
+
</div>
|
10
|
+
<div class='form-row'>
|
11
|
+
<label for='edit_url'>URL</label>
|
12
|
+
<input type='text' id='edit_url' name='url' placeholder='https://...' value="<%= @link.url %>">
|
13
|
+
</div>
|
14
|
+
<input type='submit' value='Save'>
|
15
|
+
</form>
|
16
|
+
|
17
|
+
<p>Entering a new name will create an another link for the URL. To rename completely, manually delete the old link; Be careful with renaming existing links.</p>
|
18
|
+
|
19
|
+
<hr>
|
20
|
+
|
21
|
+
<form action='<%= update_path(@link) %>' method='POST'>
|
22
|
+
<input type='hidden' name='_method' value='DELETE'>
|
23
|
+
<p><input type='submit' onclick="return confirm('Are you sure?')" class='btn-danger' value='Delete'></p>
|
24
|
+
</form>
|
25
|
+
</section>
|
26
|
+
|
data/app/views/index.erb
ADDED
@@ -0,0 +1,24 @@
|
|
1
|
+
<section class='new-link'>
|
2
|
+
<form action='/+/links' method='POST'>
|
3
|
+
<p><input type='text' name='url' placeholder='https://...' class='new-link-url'></p>
|
4
|
+
<p class='new-link-down'>⬇︎</p>
|
5
|
+
<p><input type='text' name='name' placeholder='Short name' class='new-link-name'></p>
|
6
|
+
<p><input type='submit' value='Shorten'></p>
|
7
|
+
</form>
|
8
|
+
</section>
|
9
|
+
|
10
|
+
<div class='infos'>
|
11
|
+
<% if notice_message %>
|
12
|
+
<section>
|
13
|
+
<h3>Notice</h3>
|
14
|
+
<%== notice_message %>
|
15
|
+
</section>
|
16
|
+
<% end %>
|
17
|
+
<section>
|
18
|
+
<h3>Usage</h3>
|
19
|
+
<form action='/+' method='GET'>
|
20
|
+
<p>Edit a link: <input type='text' name='show' placeholder='name...'> <input type='submit' value='Go'></p>
|
21
|
+
</form>
|
22
|
+
<p>Or add + (plus) sign after the URL like: <%= base_url %>/something → /something+</p>
|
23
|
+
</section>
|
24
|
+
</div>
|
@@ -0,0 +1,40 @@
|
|
1
|
+
<!DOCTYPE html>
|
2
|
+
<html>
|
3
|
+
<head>
|
4
|
+
<meta charset='utf-8'>
|
5
|
+
<meta name='viewport' content='width=device-width, minimum-scale=1'>
|
6
|
+
<title>Corpshort</title>
|
7
|
+
<link rel='stylesheet' href='/style.css' type="text/css">
|
8
|
+
</head>
|
9
|
+
|
10
|
+
<body>
|
11
|
+
<div class="container">
|
12
|
+
<header class="header">
|
13
|
+
<h1 class='logo'><a href="/">Corpshort</a></h1>
|
14
|
+
<nav>
|
15
|
+
<a href="/">Create</a>
|
16
|
+
<a href="/+/links">Recent</a>
|
17
|
+
</nav>
|
18
|
+
</header>
|
19
|
+
<% notice ||= session.delete(:notice); error ||= session.delete(:error) %>
|
20
|
+
<% if notice %>
|
21
|
+
<div class="notice"><%= notice %></div>
|
22
|
+
<% end %>
|
23
|
+
|
24
|
+
<% if error %>
|
25
|
+
<div class="error"><%= error %></div>
|
26
|
+
<% end %>
|
27
|
+
|
28
|
+
<div class="box">
|
29
|
+
<%== yield %>
|
30
|
+
</div>
|
31
|
+
|
32
|
+
<footer>
|
33
|
+
<div class="credit">
|
34
|
+
Powered by <a href="https://github.com/sorah/corpshort">sorah/corpshort</a>
|
35
|
+
</div>
|
36
|
+
</footer>
|
37
|
+
</div>
|
38
|
+
</body>
|
39
|
+
</html>
|
40
|
+
|
data/app/views/list.erb
ADDED
@@ -0,0 +1,11 @@
|
|
1
|
+
<h1><%= @title %></h1>
|
2
|
+
|
3
|
+
<ul>
|
4
|
+
<% @links.each do |name| %>
|
5
|
+
<li><a href='/<%= name %>+'><%= name %></li>
|
6
|
+
<% end %>
|
7
|
+
</ul>
|
8
|
+
|
9
|
+
<% if @next_token %>
|
10
|
+
<a href="<%= request.path %>?token=<%= URI.encode_www_form_component(@next_token) %>">More...</a>
|
11
|
+
<% end %>
|
data/app/views/show.erb
ADDED
@@ -0,0 +1,91 @@
|
|
1
|
+
<section class='link'>
|
2
|
+
<header>
|
3
|
+
<h2><a href="<%= short_link_url(@link) %>"><%= short_base_url.gsub(%r{\A.+://}, '') %>/<%= @link.name %></a></h2>
|
4
|
+
<a href='#' class='btn-link copy_btn'><input class='copy_btn_input' tabindex='-1' value="<%= short_link_url(@link) %>"><span class='copy_btn_text'>Copy</span></a>
|
5
|
+
<a href="<%= edit_path(@link) %>" class='btn-link'>Edit</a>
|
6
|
+
</header>
|
7
|
+
|
8
|
+
<p><a class='link-url' href="<%= @link.url %>"><%= @link.url %></a></p>
|
9
|
+
|
10
|
+
<div class='infos'>
|
11
|
+
<section>
|
12
|
+
<h3>Embed</h3>
|
13
|
+
<p>PDF is recommended for Keynote</p>
|
14
|
+
<div class='barcode-style'>
|
15
|
+
<div class='barcode-style-diagram barcode-style-diagram-cu-horizontal'>
|
16
|
+
<div class='barcode-style-diagram-code'></div>
|
17
|
+
<div class='barcode-style-diagram-url'></div>
|
18
|
+
</div>
|
19
|
+
<div class='barcode-style-links'>
|
20
|
+
<h4>Code + URL (Horizontal)</h4>
|
21
|
+
<ul>
|
22
|
+
<li><a href="<%= barcode_path(@link, 'horizontal', 'pdf', flex: true) %>">PDF</a></li>
|
23
|
+
<li><a href="<%= barcode_path(@link, 'horizontal', 'pdf') %>">PDF (Short)</a></li>
|
24
|
+
</ul>
|
25
|
+
</div>
|
26
|
+
</div>
|
27
|
+
<div class='barcode-style'>
|
28
|
+
<div class='barcode-style-diagram barcode-style-diagram-cu-vertical'>
|
29
|
+
<div class='barcode-style-diagram-code'></div>
|
30
|
+
<div class='barcode-style-diagram-url'></div>
|
31
|
+
</div>
|
32
|
+
<div class='barcode-style-links'>
|
33
|
+
<h4>Code + URL (Vertical)</h4>
|
34
|
+
<ul>
|
35
|
+
<li><a href="<%= barcode_path(@link, 'vertical', 'pdf', flex: true) %>">PDF</a></li>
|
36
|
+
<li><a href="<%= barcode_path(@link, 'vertical', 'pdf') %>">PDF (Short)</a></li>
|
37
|
+
</ul>
|
38
|
+
</div>
|
39
|
+
</div>
|
40
|
+
<div class='barcode-style'>
|
41
|
+
<div class='barcode-style-diagram barcode-style-diagram-codeonly'>
|
42
|
+
<div class='barcode-style-diagram-code'></div>
|
43
|
+
</div>
|
44
|
+
<div class='barcode-style-links'>
|
45
|
+
<h4>Code Only</h4>
|
46
|
+
<small>Code + URL is recommended</small>
|
47
|
+
<ul>
|
48
|
+
<li><a href="<%= barcode_path(@link, 'small', 'pdf') %>">PDF</a></li>
|
49
|
+
<li><a href="<%= barcode_path(@link, 'small', 'svg') %>">SVG</a></li>
|
50
|
+
<li><a href="<%= barcode_path(@link, 'small', 'png') %>">PNG</a></li>
|
51
|
+
</ul>
|
52
|
+
</div>
|
53
|
+
</div>
|
54
|
+
</section>
|
55
|
+
<section>
|
56
|
+
<h3>Metadata</h3>
|
57
|
+
<ul>
|
58
|
+
<li>Updated at: <%= @link.updated_at.utc.iso8601 %></li>
|
59
|
+
</ul>
|
60
|
+
</section>
|
61
|
+
<section>
|
62
|
+
<h3>Actions</h3>
|
63
|
+
<nav>
|
64
|
+
<p><a href="<%= urls_path(@link.url) %>">List all links for this URL</a></p>
|
65
|
+
</nav>
|
66
|
+
</section>
|
67
|
+
|
68
|
+
<script type='text/javascript'>
|
69
|
+
"use strict";
|
70
|
+
document.addEventListener("DOMContentLoaded", function() {
|
71
|
+
const copySupported = document.queryCommandSupported("copy");
|
72
|
+
document.querySelectorAll(".copy_btn").forEach(function(elem) {
|
73
|
+
if (!copySupported) {
|
74
|
+
elem.className += ' hidden';
|
75
|
+
return;
|
76
|
+
}
|
77
|
+
elem.addEventListener("click", function(e) {
|
78
|
+
const value = e.currentTarget.querySelector(".copy_btn_input");
|
79
|
+
value.focus();
|
80
|
+
value.select();
|
81
|
+
document.execCommand("copy");
|
82
|
+
e.currentTarget.querySelector(".copy_btn_text").innerHTML = "Copied!";
|
83
|
+
e.preventDefault();
|
84
|
+
});
|
85
|
+
elem.addEventListener("mouseleave", function(e) {
|
86
|
+
e.currentTarget.querySelector(".copy_btn_text").innerHTML = "Copy";
|
87
|
+
});
|
88
|
+
});
|
89
|
+
});
|
90
|
+
</script>
|
91
|
+
</section>
|
data/bin/console
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require "bundler/setup"
|
4
|
+
require "corpshort"
|
5
|
+
|
6
|
+
# You can add fixtures and/or initialization code here to make experimenting
|
7
|
+
# with your gem easier. You can also use a different console, if you like.
|
8
|
+
|
9
|
+
# (If you use this, don't forget to add pry to your Gemfile!)
|
10
|
+
# require "pry"
|
11
|
+
# Pry.start
|
12
|
+
|
13
|
+
require "irb"
|
14
|
+
IRB.start(__FILE__)
|
data/bin/setup
ADDED
data/config.ru
ADDED
@@ -0,0 +1,40 @@
|
|
1
|
+
require 'bundler/setup'
|
2
|
+
require 'securerandom'
|
3
|
+
|
4
|
+
require 'corpshort'
|
5
|
+
|
6
|
+
if ENV['RACK_ENV'] == 'production'
|
7
|
+
raise 'Set $SECRET_KEY_BASE' unless ENV['SECRET_KEY_BASE']
|
8
|
+
end
|
9
|
+
|
10
|
+
config = {
|
11
|
+
base_url: ENV['CORPSHORT_BASE_URL'],
|
12
|
+
short_base_url: ENV['CORPSHORT_SHORT_BASE_URL'],
|
13
|
+
}
|
14
|
+
|
15
|
+
case ENV.fetch('CORPSHORT_BACKEND', 'redis')
|
16
|
+
when 'redis'
|
17
|
+
require 'corpshort/backends/redis'
|
18
|
+
config[:backend] = Corpshort::Backends::Redis.new(
|
19
|
+
redis: ENV.key?('REDIS_URL') ? lambda { Redis.new(url: ENV['REDIS_URL']) } : Redis.method(:current),
|
20
|
+
prefix: ENV.fetch('CORPSHORT_REDIS_PREFIX', 'corpshort:'),
|
21
|
+
)
|
22
|
+
when 'dynamodb'
|
23
|
+
require 'corpshort/backends/dynamodb'
|
24
|
+
config[:backend] = Corpshort::Backends::Dynamodb.new(
|
25
|
+
region: ENV.fetch('CORPSHORT_DYNAMODB_REGION'),
|
26
|
+
table: ENV.fetch('CORPSHORT_DYNAMODB_TABLE'),
|
27
|
+
)
|
28
|
+
else
|
29
|
+
raise ArgumentError, "Unsupported $CORPSHORT_BACKEND"
|
30
|
+
end
|
31
|
+
|
32
|
+
use(
|
33
|
+
Rack::Session::Cookie,
|
34
|
+
key: 'corpshortsess',
|
35
|
+
expire_after: 86400,
|
36
|
+
secure: ENV.fetch('CORPSHORT_SECURE_SESSION', ENV['RACK_ENV'] == 'production' ? '1' : nil) == '1',
|
37
|
+
secret: ENV.fetch('SECRET_KEY_BASE', SecureRandom.base64(256)),
|
38
|
+
)
|
39
|
+
|
40
|
+
run Corpshort.app(config)
|
data/corpshort.gemspec
ADDED
@@ -0,0 +1,39 @@
|
|
1
|
+
|
2
|
+
lib = File.expand_path("../lib", __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require "corpshort/version"
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = "corpshort"
|
8
|
+
spec.version = Corpshort::VERSION
|
9
|
+
spec.authors = ["Sorah Fukumori"]
|
10
|
+
spec.email = ["sorah@cookpad.com"]
|
11
|
+
|
12
|
+
spec.summary = %q{"go/" like private link shortener for internal purpose}
|
13
|
+
spec.homepage = "https://github.com/sorah/corpshort"
|
14
|
+
spec.license = "MIT"
|
15
|
+
|
16
|
+
spec.files = `git ls-files -z`.split("\x0").reject do |f|
|
17
|
+
f.match(%r{^(test|spec|features)/})
|
18
|
+
end
|
19
|
+
spec.bindir = "exe"
|
20
|
+
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
|
21
|
+
spec.require_paths = ["lib"]
|
22
|
+
|
23
|
+
spec.add_dependency "sinatra"
|
24
|
+
spec.add_dependency "rack-protection"
|
25
|
+
|
26
|
+
spec.add_dependency "erubi"
|
27
|
+
|
28
|
+
spec.add_dependency "redis"
|
29
|
+
spec.add_dependency "aws-sdk-dynamodb"
|
30
|
+
|
31
|
+
spec.add_dependency "rqrcode"
|
32
|
+
spec.add_dependency "prawn"
|
33
|
+
spec.add_dependency "prawn-qrcode"
|
34
|
+
|
35
|
+
spec.add_development_dependency "bundler"
|
36
|
+
spec.add_development_dependency "rake"
|
37
|
+
spec.add_development_dependency "rspec", "~> 3.0"
|
38
|
+
spec.add_development_dependency "rack-test"
|
39
|
+
end
|
data/lib/corpshort.rb
ADDED
@@ -0,0 +1,349 @@
|
|
1
|
+
require 'rqrcode'
|
2
|
+
require 'prawn'
|
3
|
+
require 'prawn/qrcode'
|
4
|
+
|
5
|
+
require 'json'
|
6
|
+
require 'erubi'
|
7
|
+
require 'sinatra/base'
|
8
|
+
require 'rack/protection'
|
9
|
+
|
10
|
+
require 'corpshort/link'
|
11
|
+
require 'corpshort/vertical_pdf'
|
12
|
+
require 'corpshort/horizontal_pdf'
|
13
|
+
|
14
|
+
require 'uri'
|
15
|
+
|
16
|
+
module Corpshort
|
17
|
+
def self.app(*args)
|
18
|
+
App.rack(*args)
|
19
|
+
end
|
20
|
+
|
21
|
+
class App < Sinatra::Base
|
22
|
+
CONTEXT_RACK_ENV_NAME = 'corpshort.ctx'
|
23
|
+
|
24
|
+
def self.initialize_context(config)
|
25
|
+
{
|
26
|
+
config: config,
|
27
|
+
}
|
28
|
+
end
|
29
|
+
|
30
|
+
def self.rack(config={})
|
31
|
+
klass = App
|
32
|
+
|
33
|
+
test = config[:test]
|
34
|
+
session = {}
|
35
|
+
context = initialize_context(config)
|
36
|
+
lambda { |env|
|
37
|
+
env['rack.session'] = session if test # FIXME:
|
38
|
+
env[CONTEXT_RACK_ENV_NAME] = context
|
39
|
+
klass.call(env)
|
40
|
+
}
|
41
|
+
end
|
42
|
+
|
43
|
+
configure do
|
44
|
+
enable :logging
|
45
|
+
end
|
46
|
+
|
47
|
+
set :root, File.expand_path(File.join(__dir__, '..', '..', 'app'))
|
48
|
+
set :erb, :escape_html => true
|
49
|
+
|
50
|
+
use Rack::Protection::FrameOptions
|
51
|
+
use Rack::Protection::HttpOrigin
|
52
|
+
use Rack::Protection::IPSpoofing
|
53
|
+
use Rack::Protection::JsonCsrf
|
54
|
+
use Rack::Protection::PathTraversal
|
55
|
+
use Rack::Protection::RemoteToken, only_if: -> (env) { ! env['PATH_INFO'].start_with?('/+api') }
|
56
|
+
use Rack::Protection::SessionHijacking
|
57
|
+
use Rack::Protection::XSSHeader
|
58
|
+
|
59
|
+
use Rack::MethodOverride
|
60
|
+
|
61
|
+
helpers do
|
62
|
+
include Prawn::Measurements
|
63
|
+
|
64
|
+
def context
|
65
|
+
request.env[CONTEXT_RACK_ENV_NAME]
|
66
|
+
end
|
67
|
+
|
68
|
+
def conf
|
69
|
+
context.fetch(:config)
|
70
|
+
end
|
71
|
+
|
72
|
+
def notice_message
|
73
|
+
conf[:notice_message]
|
74
|
+
end
|
75
|
+
|
76
|
+
def base_url
|
77
|
+
conf[:base_url] || request.base_url
|
78
|
+
end
|
79
|
+
|
80
|
+
def short_base_url
|
81
|
+
conf[:short_base_url] || base_url
|
82
|
+
end
|
83
|
+
|
84
|
+
|
85
|
+
def backend
|
86
|
+
@backend ||= conf.fetch(:backend)
|
87
|
+
end
|
88
|
+
|
89
|
+
def link_name(name = params[:name])
|
90
|
+
name.tr('_', '-')
|
91
|
+
end
|
92
|
+
|
93
|
+
def short_link_url(link, **kwargs)
|
94
|
+
link_url(link, base_url: short_base_url, **kwargs)
|
95
|
+
end
|
96
|
+
|
97
|
+
def link_url(link, protocol: true, base_url: self.base_url())
|
98
|
+
name = link.is_a?(String) ? link_name(link) : link.name
|
99
|
+
"#{base_url}/#{name}".yield_self do |url|
|
100
|
+
if protocol
|
101
|
+
url
|
102
|
+
else
|
103
|
+
url.gsub(/\Ahttps?:\/\//, '')
|
104
|
+
end
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
108
|
+
def edit_path(link)
|
109
|
+
"/+/links/#{URI.encode_www_form_component(link.name)}/edit"
|
110
|
+
end
|
111
|
+
|
112
|
+
def update_path(link)
|
113
|
+
"/+/links/#{URI.encode_www_form_component(link.name)}"
|
114
|
+
end
|
115
|
+
|
116
|
+
def urls_path(url)
|
117
|
+
"/+/urls/#{url}"
|
118
|
+
end
|
119
|
+
|
120
|
+
def barcode_path(link, kind, ext, flex: nil)
|
121
|
+
"/+/links/#{URI.encode_www_form_component(link.name)}/#{kind}.#{ext}#{flex.nil? ? nil : "?flex=#{flex}"}"
|
122
|
+
end
|
123
|
+
end
|
124
|
+
|
125
|
+
get '/' do
|
126
|
+
erb :index
|
127
|
+
end
|
128
|
+
|
129
|
+
## Pages
|
130
|
+
|
131
|
+
get '/+' do
|
132
|
+
if params[:show]
|
133
|
+
redirect "/+/links/#{link_name(params[:show])}"
|
134
|
+
end
|
135
|
+
halt 404
|
136
|
+
end
|
137
|
+
|
138
|
+
post '/+/links' do
|
139
|
+
unless params[:name] && params[:url]
|
140
|
+
session[:error] = "Name and URL are required"
|
141
|
+
redirect '/'
|
142
|
+
end
|
143
|
+
|
144
|
+
begin
|
145
|
+
link = Link.new({name: link_name, url: params[:url]})
|
146
|
+
link.save!(backend, create_only: true)
|
147
|
+
rescue Corpshort::Link::ValidationError, Corpshort::Backends::Base::ConflictError
|
148
|
+
session[:error] = $!.message
|
149
|
+
redirect '/'
|
150
|
+
end
|
151
|
+
|
152
|
+
redirect "/#{link.name}+"
|
153
|
+
end
|
154
|
+
|
155
|
+
get '/+/links' do
|
156
|
+
@links, @next_token = backend.list_links(token: params[:token])
|
157
|
+
@title = "Recent links"
|
158
|
+
erb :list
|
159
|
+
end
|
160
|
+
|
161
|
+
get '/+/links/*name/small.svg' do
|
162
|
+
@link = backend.get_link(params[:name])
|
163
|
+
halt 404, "not found" unless @link
|
164
|
+
|
165
|
+
content_type :svg
|
166
|
+
RQRCode::QRCode.new(link_url(@link), level: :m).as_svg(module_size: 6)
|
167
|
+
end
|
168
|
+
get '/+/links/*name/small.png' do
|
169
|
+
@link = backend.get_link(params[:name])
|
170
|
+
|
171
|
+
halt 404, "not found" unless @link
|
172
|
+
content_type :png
|
173
|
+
RQRCode::QRCode.new(link_url(@link), level: :m).as_png(size: 120).to_datastream.to_s
|
174
|
+
end
|
175
|
+
get '/+/links/*name/small.pdf' do
|
176
|
+
@link = backend.get_link(params[:name])
|
177
|
+
halt 404, "not found" unless @link
|
178
|
+
|
179
|
+
content_type :pdf
|
180
|
+
Prawn::Document.new(page_size: [cm2pt(2), cm2pt(2)], margin: 0) do |pdf|
|
181
|
+
pdf.fill_color 'FFFFFF'
|
182
|
+
pdf.fill { pdf.rounded_rectangle [cm2pt(2), cm2pt(2)], cm2pt(2), cm2pt(2), 10 }
|
183
|
+
pdf.print_qr_code(link_url(@link), level: :m, extent: cm2pt(2), stroke: false)
|
184
|
+
end.render
|
185
|
+
end
|
186
|
+
|
187
|
+
get '/+/links/*name/vertical.pdf' do
|
188
|
+
@link = backend.get_link(params[:name])
|
189
|
+
halt 404, "not found" unless @link
|
190
|
+
|
191
|
+
content_type :pdf
|
192
|
+
|
193
|
+
VerticalPdf.new(
|
194
|
+
url: link_url(@link),
|
195
|
+
base_url: short_base_url.sub(%r{\A.+://}, ''),
|
196
|
+
name: @link.name,
|
197
|
+
flex: params[:flex],
|
198
|
+
).document.render
|
199
|
+
end
|
200
|
+
|
201
|
+
get '/+/links/*name/horizontal.pdf' do
|
202
|
+
@link = backend.get_link(params[:name])
|
203
|
+
halt 404, "not found" unless @link
|
204
|
+
|
205
|
+
content_type :pdf
|
206
|
+
HorizontalPdf.new(
|
207
|
+
url: link_url(@link),
|
208
|
+
base_url: short_base_url.sub(%r{\A.+://}, ''),
|
209
|
+
name: @link.name,
|
210
|
+
flex: params[:flex],
|
211
|
+
).document.render
|
212
|
+
end
|
213
|
+
|
214
|
+
|
215
|
+
get '/+/links/*name/edit' do
|
216
|
+
@link = backend.get_link(params[:name])
|
217
|
+
if @link
|
218
|
+
erb :edit
|
219
|
+
else
|
220
|
+
halt 404, "not found"
|
221
|
+
end
|
222
|
+
end
|
223
|
+
|
224
|
+
get '/+/links/*name' do
|
225
|
+
redirect "/#{params[:name]}+"
|
226
|
+
end
|
227
|
+
|
228
|
+
put '/+/links/*name' do
|
229
|
+
@link = backend.get_link(params[:name])
|
230
|
+
halt 404, "not found" unless @link
|
231
|
+
|
232
|
+
@link.url = params[:url] if params[:url]
|
233
|
+
|
234
|
+
rename = params[:new_name] && @link.name != params[:new_name]
|
235
|
+
if rename
|
236
|
+
new_name = link_name(params[:new_name])
|
237
|
+
@link = Link.new(name: new_name, url: @link.url)
|
238
|
+
# Link.validate_name(new_name)
|
239
|
+
# backend.rename_link(@link, new_name)
|
240
|
+
end
|
241
|
+
|
242
|
+
begin
|
243
|
+
@link.save!(backend, create_only: rename)
|
244
|
+
rescue Corpshort::Link::ValidationError, Corpshort::Backends::Base::ConflictError
|
245
|
+
session[:error] = $!.message
|
246
|
+
redirect "/+/links/#{@link.name}/edit"
|
247
|
+
end
|
248
|
+
|
249
|
+
redirect "/#{@link.name}+"
|
250
|
+
end
|
251
|
+
|
252
|
+
delete '/+/links/*name' do
|
253
|
+
backend.delete_link(params[:name])
|
254
|
+
redirect "/"
|
255
|
+
end
|
256
|
+
|
257
|
+
get '/+/urls/*url' do
|
258
|
+
url = env['REQUEST_URI'][8..-1]
|
259
|
+
@links, @next_token = backend.list_links_by_url(url), nil
|
260
|
+
@title = "Links for URL #{url}"
|
261
|
+
erb :list
|
262
|
+
end
|
263
|
+
|
264
|
+
## API
|
265
|
+
|
266
|
+
get '/+api/links' do
|
267
|
+
content_type :json
|
268
|
+
links, next_token = backend.list_links(token: params[:token])
|
269
|
+
{links: links, next_token: next_token}.to_json
|
270
|
+
end
|
271
|
+
|
272
|
+
post '/+api/links' do
|
273
|
+
content_type :json
|
274
|
+
|
275
|
+
unless params[:name] && params[:url]
|
276
|
+
halt 400, '{"error": "missing_params", "error_message": "name and url are required"}'
|
277
|
+
end
|
278
|
+
|
279
|
+
begin
|
280
|
+
link = Link.new({name: link_name, url: params[:url]})
|
281
|
+
link.save!(backend, create_only: true)
|
282
|
+
rescue Corpshort::Link::ValidationError => e
|
283
|
+
halt(400, {error: :validation_error, error_message: e.message}.to_json)
|
284
|
+
rescue Corpshort::Backends::Base::ConflictError
|
285
|
+
halt(409, {error: :conflict, error_message: e.message}.to_json)
|
286
|
+
end
|
287
|
+
|
288
|
+
link.to_json
|
289
|
+
end
|
290
|
+
|
291
|
+
get '/+api/links/*name' do
|
292
|
+
content_type :json
|
293
|
+
link = backend.get_link(link_name)
|
294
|
+
halt 404, '{"error": "not_found"}' unless link
|
295
|
+
link.to_json
|
296
|
+
end
|
297
|
+
|
298
|
+
put '/+api/links/*name' do
|
299
|
+
content_type :json
|
300
|
+
link = backend.get_link(link_name)
|
301
|
+
halt 404, '{"error": "not_found"}' unless link
|
302
|
+
link.url = params[:url] if params[:url]
|
303
|
+
|
304
|
+
begin
|
305
|
+
link.save!(backend)
|
306
|
+
rescue Corpshort::Link::ValidationError => e
|
307
|
+
halt(400, {error: :validation_error, error_message: e.message}.to_json)
|
308
|
+
rescue Corpshort::Backends::Base::ConflictError
|
309
|
+
halt(409, {error: :conflict, error_message: e.message}.to_json)
|
310
|
+
end
|
311
|
+
link.to_json
|
312
|
+
end
|
313
|
+
|
314
|
+
delete '/+api/links/*name' do
|
315
|
+
backend.delete_link(link_name)
|
316
|
+
status 202
|
317
|
+
""
|
318
|
+
end
|
319
|
+
|
320
|
+
get '/+api/urls/*url' do
|
321
|
+
content_type :json
|
322
|
+
url = env['REQUEST_URI'][8..-1]
|
323
|
+
links, next_token = backend.list_links_by_url(url), nil
|
324
|
+
{links: links, next_token: next_token}.to_json
|
325
|
+
end
|
326
|
+
|
327
|
+
## Shortlink
|
328
|
+
|
329
|
+
get '/*name' do
|
330
|
+
name = params[:name]
|
331
|
+
show = name.end_with?('+')
|
332
|
+
if show
|
333
|
+
name = name[0..-2]
|
334
|
+
end
|
335
|
+
|
336
|
+
@link = backend.get_link(link_name(name))
|
337
|
+
|
338
|
+
unless @link
|
339
|
+
halt 404, 'not found'
|
340
|
+
end
|
341
|
+
|
342
|
+
if show
|
343
|
+
erb :show
|
344
|
+
else
|
345
|
+
redirect @link.url
|
346
|
+
end
|
347
|
+
end
|
348
|
+
end
|
349
|
+
end
|