diff --git a/app.yaml b/app.yaml new file mode 100644 index 0000000..24d65d9 --- /dev/null +++ b/app.yaml @@ -0,0 +1,12 @@ +application: urly2 +version: 1 +runtime: python +api_version: 1 + +handlers: +- url: /static + static_dir: static + +- url: .* + script: main.py + \ No newline at end of file diff --git a/index.yaml b/index.yaml new file mode 100644 index 0000000..4769511 --- /dev/null +++ b/index.yaml @@ -0,0 +1,19 @@ +indexes: + +# +#- kind: Urly +# properties: +# - name: href +# Creating a composite index failed: ascending single-property indexes are not necessary +# + + +# AUTOGENERATED + +# This index.yaml is automatically updated whenever the dev_appserver +# detects that a new type of query is run. If you want to manage the +# index.yaml file manually, remove the above marker line (the line +# saying "# AUTOGENERATED"). If you want to manage some indexes +# manually, move them above the marker line. The index.yaml file is +# automatically uploaded to the admin console when you next deploy +# your application using appcfg.py. diff --git a/main.html b/main.html new file mode 100644 index 0000000..ab6a296 --- /dev/null +++ b/main.html @@ -0,0 +1,41 @@ +{% comment %} +// Variables for this template: +// urly - this is an urly instance to show (we use code and href properties) +// error_404 - an error message to show the user +// default_href - default href to dump into text area. Why do we have this? +// error_href - set when we have an improperly formatted href +{% endcomment %} + + +ur.ly - dang short urls + + + + +
+ + + - dang short urls + +{% if error_404 or urly or error_href %} +
+ {% if error_404 %}Oops - we couldn't find that urly.{% endif %} + {% if error_href %}Oops - that address doesn't look right... we can't create a dang short url from it.{% endif %} + {% if urly %} + {{ urly.href }} is now dang short, and its already on your clipboard.
+ + + +   Try It + {% endif %} +
+{% endif %} + +
Enter a long URL below and we'll make it dang short:
+
+ + +
+
Project APIDrag this link to your browser toolbar: ur.ly!
+
+ \ No newline at end of file diff --git a/main.py b/main.py new file mode 100755 index 0000000..762496d --- /dev/null +++ b/main.py @@ -0,0 +1,71 @@ +#!/usr/bin/env python +# +# Copyright 2008 Adam Stiles +# Licensed under the Apache License, Version 2.0 (the "License"); you may not +# use this file except in compliance with the License. You may obtain a copy +# of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required +# by applicable law or agreed to in writing, software distributed under the +# License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS +# OF ANY KIND, either express or implied. See the License for the specific +# language governing permissions and limitations under the License. +# + +"""A url-shortener built on Google App Engine.""" +__author__ = 'Adam Stiles' + +""" +All Urly records in the database have an id and an href. We base62 that +integer id to create a short code that represents that Urly. + +/{code} Redirect user to urly with this code +/{code}(.json|.xml|.html) Show user formatted urly with this code +/new(.json|.xml|.html)?href={href} Create a new urly with this href or + return existing one if it already exists + Note special handling for 'new' code + when we have a href GET parameter 'cause + 'new' by itself looks like a code +""" +import wsgiref.handlers +import re, os, logging +from google.appengine.ext import webapp +from google.appengine.ext import db +from urly import Urly +from view import MainView + +class MainHandler(webapp.RequestHandler): + """All non-static requests go through this handler. + The code and format parameters are pre-populated by + our routing regex... see main() below. + """ + def get(self, code, format): + if (code is None): + MainView.render(self, 200, None, format) + return + + href = self.request.get('href').strip() + if (code == 'new') and (href is not None): + try: + u = Urly.find_or_create_by_href(href) + if u is not None: + MainView.render(self, 200, u, format) + else: + logging.error("Error creating urly by href: %s", str(href)) + MainView.render(self, 400, None, format, href) + except db.BadValueError: + # href parameter is bad + MainView.render(self, 400, None, format, href) + else: + u = Urly.find_by_code(str(code)) + if u is not None: + MainView.render(self, 200, u, format) + else: + MainView.render(self, 404, None, format) + +def main(): + application = webapp.WSGIApplication([ + ('/([a-zA-Z0-9]{1,6})?(.xml|.json|.html)?', MainHandler) + ], debug=True) + wsgiref.handlers.CGIHandler().run(application) + +if __name__ == '__main__': + main() diff --git a/static/clipboard.swf b/static/clipboard.swf new file mode 100755 index 0000000..2cfe371 Binary files /dev/null and b/static/clipboard.swf differ diff --git a/static/urly.gif b/static/urly.gif new file mode 100644 index 0000000..0e4ee5a Binary files /dev/null and b/static/urly.gif differ diff --git a/static/urly.js b/static/urly.js new file mode 100644 index 0000000..efa23d4 --- /dev/null +++ b/static/urly.js @@ -0,0 +1,29 @@ +//----------------------------------------------------------------------------- +function copy(text2copy) { + if (window.clipboardData) { + window.clipboardData.setData("Text",text2copy); + } else { + var flashcopier = 'flashcopier'; + if(!document.getElementById(flashcopier)) { + var divholder = document.createElement('div'); + divholder.id = flashcopier; + document.body.appendChild(divholder); + } + document.getElementById(flashcopier).innerHTML = ''; + var divinfo = ''; + document.getElementById(flashcopier).innerHTML = divinfo; + } +} + +//----------------------------------------------------------------------------- +function focus(id) +{ + var el = document.getElementById(id); + if (el != null) { + el.focus(); + el.select(); + } +} + +//----------------------------------------------------------------------------- + diff --git a/urly.py b/urly.py new file mode 100644 index 0000000..0a2065b --- /dev/null +++ b/urly.py @@ -0,0 +1,88 @@ +# Copyright 2008 Adam Stiles +# Licensed under the Apache License, Version 2.0 (the "License"); you may not +# use this file except in compliance with the License. You may obtain a copy +# of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required +# by applicable law or agreed to in writing, software distributed under the +# License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS +# OF ANY KIND, either express or implied. See the License for the specific +# language governing permissions and limitations under the License. + +from google.appengine.ext import db +from google.appengine.api import memcache +import logging + +class Urly(db.Model): + """Our one-and-only model""" + href = db.LinkProperty(required=True) + created_at = db.DateTimeProperty(auto_now_add=True) + + KEY_BASE = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ" + BASE = 62 + + def code(self): + """Return our code, our base-62 encoded id""" + if not self.is_saved(): + return None + nid = self.key().id() + s = [] + while nid: + nid, c = divmod(nid, Urly.BASE) + s.append(Urly.KEY_BASE[c]) + s.reverse() + return "".join(s) + + def to_json(self): + """JSON is so simple that we won't worry about a template at this point""" + return "{\"code\":\"%s\",\"href\":%s\"}\n" % (self.code(), self.href); + + def to_xml(self): + """Like JSON, XML is simple enough that we won't template now""" + msg = "\n" + msg += "\n" % (self.code(), self.href) + return msg + + def save_in_cache(self): + """We don't really care if this fails""" + memcache.set(self.code(), self) + + @staticmethod + def find_or_create_by_href(href): + query = db.Query(Urly) + query.filter('href =', href) + u = query.get() + if not u: + u = Urly(href=href) + u.put() + u.save_in_cache() + return u + + @staticmethod + def code_to_id(code): + aid = 0L + for c in code: + aid *= Urly.BASE + aid += Urly.KEY_BASE.index(c) + return aid + + @staticmethod + def find_by_code(code): + try: + u = memcache.get(code) + except: + # http://code.google.com/p/googleappengine/issues/detail?id=417 + logging.error("Urly.find_by_code() memcached error") + u = None + + if u is not None: + logging.info("Urly.find_by_code() cache HIT: %s", str(code)) + return u + + logging.info("Urly.find_by_code() cache MISS: %s", str(code)) + aid = Urly.code_to_id(code) + try: + u = Urly.get_by_id(int(aid)) + if u is not None: + u.save_in_cache() + return u + except db.BadValueError: + return None \ No newline at end of file diff --git a/view.py b/view.py new file mode 100644 index 0000000..94f417b --- /dev/null +++ b/view.py @@ -0,0 +1,47 @@ +# Copyright 2008 Adam Stiles +# Licensed under the Apache License, Version 2.0 (the "License"); you may not +# use this file except in compliance with the License. You may obtain a copy +# of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required +# by applicable law or agreed to in writing, software distributed under the +# License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS +# OF ANY KIND, either express or implied. See the License for the specific +# language governing permissions and limitations under the License. + +import os +from google.appengine.ext.webapp import template + +class MainView(): + """Helper method for our one-and-only template. All display goes through here""" + @staticmethod + def render(handler, status, urly, format, href=None): + """Lovin my delphi-like inner functions""" + def render_raw(handler, content_type, body): + handler.response.headers["Content-Type"] = content_type + handler.response.out.write(body) + + def render_main(handler, values=None): + path = os.path.join(os.path.dirname(__file__), 'main.html') + handler.response.out.write(template.render(path, values)) + + """ We never have an error if we have an urly to show """ + if (urly is not None): + if (format is None): + handler.redirect(urly.href) + elif (format == '.json'): + render_raw(handler, "application/json", urly.to_json()) + elif (format == '.xml'): + render_raw(handler, "application/xml", urly.to_xml()) + else: + render_main(handler, { 'urly': urly }) + elif (status == 400): + handler.error(status) + if (format != '.json') and (format != '.xml'): + vals = { 'error_href': True, 'default_href': href } + render_main(handler, vals) + elif (status == 404): + handler.error(404) + if (format != '.json') and (format != '.xml'): + vals = { 'error_404': True } + render_main(handler, vals) + else: + render_main(handler)