forked from rossdylan/helix.ly
-
Notifications
You must be signed in to change notification settings - Fork 0
/
helixly.py
186 lines (154 loc) · 6.26 KB
/
helixly.py
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
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
import md5
from bottle import abort, redirect, request, route, run
import json
from shove import Shove
from time import ctime
def hashLink(link):
"""
Hash a link, this might need to be changed to be more complex at some point
:type link: str
:param link: The link to be hashed
"""
return str(md5.new(link).hexdigest())[:5]
def cache(func, cache, invalid_after):
"""
Caching function which stores cached data in a dict (or a shove object)
:type func: function
:param func: Function whose output should be cached
:type cache: dict
:param cache: Anything that implements the same methods as dict, it stores our cached data
:type invalid_after: int
:param invalid_after: time in seconds to wait before invaliding cache data
"""
def cache_wrapper(*args, **kwargs):
call_id = str(func) + str(args)
try:
return_value = cache[call_id]
if ctime() - return_value[0] > invalid_after:
raise Exception
else:
return return_value[1]
except:
return_value = func(*args, **kwargs)
cache[call_id] = (ctime(), return_value)
return return_value
return cache_wrapper
class CSHLYServer(object):
"""
WSGI Server exposing the link shortening api
:type port: int
:param port: port to listen for connections on
:type link_db_uri: str
:param link_db_uri: a shove compatible database uri for storing shortened links
:type user_db_uri: str
:param user_db_uri: a shove compatible database uri for storing user information
:type use_auth: boolean
:param use_auth: Enable authentication or disable authentication, default is enabled
"""
def __new__(self, *args, **kwargs):
"""
Used to call decorators on all the functions in the class
shorten -> /api/shorten
unshorten -> /api/unshorten/<hashed>
unshorten_redirect -> /<hashed>
get_link_data is cached
"""
obj = super(CSHLYServer, self).__new__(self, *args, **kwargs)
obj.cache = Shove()
obj.unshorten = cache(obj.unshorten, obj.cache, 300)
route("/api/shorten", method='PUT')(obj.shorten)
route("/api/unshorten/<hashed>", method='GET')(obj.unshorten)
route("/<hashed>", method='GET')(obj.unshorten_redirect)
obj.get_link_data = cache(obj.get_link_data, obj.cache, 1200)
return obj
def __init__(self, port, link_db_uri, user_db_uri, use_auth=True):
self.port = port
self.link_db = Shove(link_db_uri)
self.user_db = Shove(user_db_uri)
self.use_auth = use_auth
if not self.use_auth and 'null' not in self.user_db:
self.user_db['null'] = {'token': '', 'username':'null', 'links': []}
def get_link_data(self, hashed_link):
"""
Used to get information on a hashed link. This function is cached
:type hashed_link: str
:param hashed_link: the hashed link to retrieve information on
"""
try:
data = self.link_db[hashed_link]
return data
except:
return None
def is_user_authenticated(self, user_id, auth_token):
"""
Check to see if a user is authenticated or not
:type user_id: str
:param user_id: the users ID
:type auth_token: str
:param auth_token: a token used to see if a user is valid
"""
user = self.user_db[user_id]
if user['token'] == auth_token:
return True
else:
return False
def shorten(self):
"""
Used to handle the shorten api endpoint
The body of the request contains the json formatted request to shorten a url
this function returns a json formatted response with the shortened url
"""
data = request.body.readline()
print "Received shorten request: {0}".format(data)
if not data:
abort(400, 'No data received')
data = json.loads(data)
if "full_link" in data:
if ("user_id" in data and "auth_token" in data and self.is_user_authenticated(data['user_id'], data['auth_token'])) or not self.use_auth:
hashed = hashLink(data['full_link'])
self.link_db[hashed] = {'lookups':0, 'owner': data.get('user_id','null'), 'full_link': data['full_link']}
self.link_db.sync()
try:
self.user_db[data.get('user_id','null')]['links'].append(hashed)
except:
self.user_db[data.get('user_id','null')]['links'] = [hashed,]
self.user_db.sync()
return json.dumps({"shortened": hashed})
else:
abort(403, 'User id or auth token incorrect')
def unshorten(self, hashed):
"""
Used to handle the unshorten api endpoint
This function is given a url hash and returns a json formatted response with information on that url
"""
print "Received unshorten request: {0}".format(hashed)
link_data = self.get_link_data(hashed)
if link_data == None:
return json.dumps({'error': 'Link not Found'})
else:
self.link_db[hashed]['lookups'] += 1
self.link_db.sync()
return json.dumps({'full_link': link_data['full_link'], 'lookups': link_data['lookups']})
def unshorten_redirect(self, hashed):
"""
Used to unshorten a hashed url given to the function and redirect the user to its destination
Currently doesn't support https (need to fix that >_>)
example: http://helix.ly/hashed_url -> http://full-url.com/some_random_page
"""
link_data = self.get_link_data(hashed)
if link_data == None:
abort(404, 'Shortened URL not found')
else:
self.link_db[hashed]['lookups'] += 1
redirect("http://" + link_data['full_link'])
self.link_db.sync()
def start(self):
"""
Called to start the wsgi server
"""
run(server='eventlet', port=self.port)
self.link_db.sync()
self.user_db.sync()
if __name__ == "__main__":
shortener = CSHLYServer(8080, "file://links.db", "file://users.db", use_auth=False)
shortener.start()