Commits

Miki Tebeka committed 22959b5

AppEngine demo

  • Participants
  • Parent commits 12d5b39

Comments (0)

Files changed (10)

File appengine-demo/README.txt

+This is a demo on using Go with AppEngine.
+
+The application here is a URL shortener (like bit.ly). It demos the following
+features:
+- HTTP serving
+- Templates
+- Datastore
+- Task Queues
+- Logging
+- Memcache
+
+See SDK documentation at http://code.google.com/appengine/docs/go/gettingstarted/

File appengine-demo/app.yaml

+application: go-shortie
+version: 1
+runtime: go
+api_version: 3
+
+handlers:
+- url: /static
+  static_dir: static
+- url: /.*
+  script: _go_app

File appengine-demo/compile.sh

+#!/bin/bash
+
+# Compile the code
+
+cd shortie
+GOROOT=/opt/google_appengine_go/goroot \
+    /opt/google_appengine_go/goroot/bin/6g *.go

File appengine-demo/index.yaml

+indexes:
+
+# 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.
+
+- kind: Url
+  properties:
+  - name: User
+  - name: Created
+    direction: desc

File appengine-demo/push.sh

+#!/bin/bash
+# Push application to AppEngine and update deployment tag
+
+# Exit on error
+set -e
+
+/opt/google_appengine_go/appcfg.py update .
+hg tag -f appengine
+hg push

File appengine-demo/run-local.sh

+#!/bin/bash
+# Run server locally
+
+/opt/google_appengine_go/dev_appserver.py . $@

File appengine-demo/shortie/html.go

+package shortie
+
+const homeHTML = `
+<html>
+    <head>
+        <title>Shortie - The URL Shortener</title>
+        <link rel="stylesheet" href="/static/style.css" />
+        <link rel="shortcut icon" href="/static/logo.png" />
+    </head>
+    <body>
+        <div class="header">
+            The URL Shortener
+            <span class="right">
+				<a href="{{ .LoginURL }}">{{ .LoginTitle }}</a>
+            </span>
+        </div>
+
+        {{ if .ShortURL }}
+        <div class="short">
+            Url shortened to <a href="{{ .ShortURL }}">{{ .ShortURL }}</a>
+        </div>
+        {{ end }}
+
+        {{ if .Error }}
+        <div class="error">
+            ERROR: {{ .Error | html}}
+        </div>
+        {{ end }}
+
+        <form method="post">
+            <label for="url">Url:</label><input name="url" /> 
+            <input type="submit" value="Shorten">
+        </form>
+
+		{{ with .URLs }}
+        <b>Your Urls</b>
+        <table class="urls">
+            <tr><th>URL</th><th>Created</th><th>Hits</th></tr>
+			{{ range $idx, $url := . }}
+				<tr class="{{ $idx | oddeven }}">
+					<td title="{{ .Long | html }}">
+						<a href="{{ .Long }}">{{ .Short }}</a>
+					</td>
+					<td>{{ .Created | strftime }}</td>
+					<td>{{ .Hits }}</td>
+            </tr>
+            {{ end }}
+        </table>
+        {{ end }}
+
+        <div class="footer">
+            G'day {{ .User }}, we've shortened {{ .Count }} urls so far.
+        </div>
+	</body>
+</html>
+`
+
+// vim: ft=html

File appengine-demo/shortie/shortie.go

+/* AppEngine demo - a URL shortener */
+package shortie
+
+import (
+	"appengine"
+	"appengine/datastore"
+	"appengine/delay"
+	"appengine/memcache"
+	"appengine/user"
+	"fmt"
+	"http"
+	"os"
+	"regexp"
+	"strings"
+	"template"
+	"time"
+)
+
+const (
+	base62Alphabet = "abcdefghijklmnopqrstuvwxyz0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ"
+	counterKeyName = "counter-key-name"
+	counterKind    = "Counter"
+	urlKind        = "Url"
+	hitWorkerPath  = "/_worker/hit"
+)
+
+// base62Encode encodes a number to a base62 string representation.
+func base62Encode(num uint64) string {
+	if num == 0 {
+		return "0"
+	}
+
+	arr := []uint8{}
+	base := uint64(len(base62Alphabet))
+
+	for num > 0 {
+		rem := num % base
+		num = num / base
+		arr = append(arr, base62Alphabet[rem])
+	}
+
+	for i, j := 0, len(arr)-1; i < j; i, j = i+1, j-1 {
+		arr[i], arr[j] = arr[j], arr[i]
+	}
+
+	return string(arr)
+}
+
+type Counter struct {
+	Count int64
+}
+
+// urlCount return the current count of urls.
+func urlCount(ctx appengine.Context) (int64, os.Error) {
+	key := datastore.NewKey(ctx, counterKind, counterKeyName, 0, nil)
+	counter := new(Counter)
+	if err := datastore.Get(ctx, key, counter); err != nil && err != datastore.ErrNoSuchEntity {
+		return 0, err
+	}
+
+	return counter.Count, nil
+}
+
+/* nextId returns the next short url. 
+We use the global counter and then encode the last count in base62.
+*/
+func nextId(ctx appengine.Context) (string, os.Error) {
+	var count int64
+
+	err := datastore.RunInTransaction(ctx, func(ctx appengine.Context) os.Error {
+		key := datastore.NewKey(ctx, counterKind, counterKeyName, 0, nil)
+		counter := new(Counter)
+		if err := datastore.Get(ctx, key, counter); err != nil && err != datastore.ErrNoSuchEntity {
+			return err
+		}
+
+		counter.Count++
+		if _, err := datastore.Put(ctx, key, counter); err != nil {
+			return err
+		}
+		count = counter.Count
+		return nil
+	}, nil)
+
+	return base62Encode(uint64(count)), err
+}
+
+type URL struct {
+	Short   string
+	Long    string
+	User    string
+	Created int64
+	Hits    int64
+}
+
+// urlKey returns a datastore key for short url.
+func urlKey(ctx appengine.Context, short string) *datastore.Key {
+	return datastore.NewKey(ctx, urlKind, short, 0, nil)
+}
+
+// getURL fetches a URL from the datastore by short url.
+func getURL(ctx appengine.Context, short string) (*URL, os.Error) {
+	key := urlKey(ctx, short)
+	url := new(URL)
+	err := datastore.Get(ctx, key, url)
+
+	return url, err
+}
+
+// incHits increments hit count on url (this is done when short url is resolved).
+func incHits(ctx appengine.Context, short string) os.Error {
+	return datastore.RunInTransaction(ctx, func(ctx appengine.Context) os.Error {
+		url, err := getURL(ctx, short)
+		if err != nil {
+			return err
+		}
+
+		url.Hits++
+		key := urlKey(ctx, short)
+		if _, err := datastore.Put(ctx, key, url); err != nil {
+			return err
+		}
+		return nil
+	}, nil)
+}
+
+// delayedInc is a "delayed" call to incHits.
+var delayedInc = delay.Func("hits", incHits)
+
+// userURLs return a list of user urls, ordered by time (max 1000).
+func userURLs(ctx appengine.Context, user string) ([]*URL, os.Error) {
+	query := datastore.NewQuery(urlKind).
+		Filter("User =", user).
+		Order("-Created").
+		Limit(1000)
+	buff := []*URL{}
+	it := query.Run(ctx)
+
+	for {
+		var url URL
+		_, err := it.Next(&url)
+		if err == datastore.Done {
+			break
+		}
+		// FIXME: Handle NEED_INDEX (when index is built)
+		if err != nil {
+			return nil, err
+		}
+		buff = append(buff, &url)
+	}
+
+	return buff, nil
+}
+
+var homeTemplate *template.Template
+
+func init() {
+	var fmap = template.FuncMap{
+		"strftime": timeFormatter,
+		"oddeven" : oddEven,
+	}
+	// homeHTML is in html.go
+	homeTemplate = template.Must(template.New("home").Funcs(fmap).Parse(homeHTML))
+	http.HandleFunc(hitWorkerPath, hitHandler)
+	http.HandleFunc("/", rootHandler)
+}
+
+// Parameters for homeTemplate.
+type homeParams struct {
+	User       string
+	LoginTitle string
+	LoginURL   string
+	Count      int64
+	Error      string
+	ShortURL   string
+	URLs       []*URL
+}
+
+/* fullURL adds http://<host> suffix to short url. 
+This works both locally and on AppEngine.
+*/
+func fullURL(r *http.Request, id string) string {
+	return fmt.Sprintf("http://%s/%s", r.Host, id)
+}
+
+// hasSchema check if url has schema prefix.
+func hasSchema(url string) bool {
+	match, _ := regexp.MatchString("^[a-zA-Z]+://", url)
+	return match
+}
+
+// rootHandler handles the main page.
+func rootHandler(w http.ResponseWriter, r *http.Request) {
+	// If it has something, we assume it's a short url
+	if r.URL.Path != "/" {
+		redirectHandler(w, r)
+		return
+	}
+
+	ctx := appengine.NewContext(r)
+	var err os.Error
+	params := &homeParams{
+		User: "stranger",
+	}
+
+	// Run at end. We check "err" and update params if needed. Then serve homeTemplate.
+	defer func() {
+		if err != nil {
+			params.Error = err.String()
+			ctx.Errorf("%v", err)
+		}
+		homeTemplate.Execute(w, params)
+	}()
+
+	u := user.Current(ctx)
+
+	if u != nil {
+		params.User = u.String()
+		params.LoginTitle = "Logout"
+		params.LoginURL, err = user.LogoutURL(ctx, r.URL.String())
+
+		params.URLs, err = userURLs(ctx, params.User)
+		if err != nil {
+			return
+		}
+	} else {
+		params.LoginTitle = "Login"
+		params.LoginURL, err = user.LoginURL(ctx, r.URL.String())
+	}
+
+	if r.Method == "POST" {
+		longURL := strings.TrimSpace(r.FormValue("url"))
+		if len(longURL) == 0 {
+			err = os.NewError("Empty URL")
+			return
+		}
+
+		if !hasSchema(longURL) {
+			longURL = fmt.Sprintf("http://%s", longURL)
+		}
+
+		if !strings.Contains(longURL, ".") {
+			err = os.NewError(fmt.Sprintf("Bad URL - %s", longURL))
+			return
+		}
+
+		var id string
+		id, err = nextId(ctx)
+		if err != nil {
+			return
+		}
+
+		url := &URL{
+			Short:   id,
+			Long:    longURL,
+			User:    params.User,
+			Created: time.Nanoseconds(),
+			Hits:    0,
+		}
+		key := datastore.NewKey(ctx, urlKind, id, 0, nil)
+		_, err = datastore.Put(ctx, key, url)
+		if err != nil {
+			return
+		}
+		params.ShortURL = fullURL(r, id)
+	}
+
+	params.Count, err = urlCount(ctx)
+}
+
+/* redirectHandler handles redirects.
+All urls that are not / and worker are assumed to be redirects (short).
+*/
+func redirectHandler(w http.ResponseWriter, r *http.Request) {
+	ctx := appengine.NewContext(r)
+	path := r.URL.Path[1:]
+
+	var longURL string
+
+	// Try memcache first and if not get URL from datastore and updatge memcache
+	if item, err := memcache.Get(ctx, path); err == memcache.ErrCacheMiss {
+		url, err1 := getURL(ctx, path)
+		if err1 != nil {
+			http.NotFound(w, r)
+			return
+		}
+		longURL = url.Long
+
+		item1 := &memcache.Item{
+			Key:   path,
+			Value: []byte(longURL),
+		}
+
+		if err1 := memcache.Set(ctx, item1); err1 != nil {
+			ctx.Errorf("memcache setting error: %v", err1)
+		}
+	} else if err != nil {
+		ctx.Errorf("memcache error - %s", err)
+		http.Error(w, err.String(), http.StatusInternalServerError)
+		return
+	} else { // Found in memcache
+		longURL = string(item.Value)
+	}
+
+	// Offload hit counter update to a task
+	delayedInc.Call(ctx, path)
+
+	http.Redirect(w, r, longURL, http.StatusTemporaryRedirect)
+}
+
+// timeFormatter formats time. It is used by homeTemplate.
+func timeFormatter(args ...interface{}) string {
+	nsec, ok := args[0].(int64)
+	if !ok {
+		return "ERR"
+	}
+	t := time.NanosecondsToLocalTime(nsec)
+	return t.Format("2006-01-02 15:04")
+}
+
+// oddEven picks class for <tr> in homeTemplate.
+func oddEven(args ...interface{}) string {
+	idx, ok := args[0].(int)
+	if !ok {
+		return "ERR"
+	}
+	if idx % 2 == 0 {
+		return "odd"
+	}
+	return "even"
+}
+
+// hitHandler is handler for updating hits task.
+func hitHandler(w http.ResponseWriter, r *http.Request) {
+	ctx := appengine.NewContext(r)
+	path := r.FormValue("path")
+	if len(path) == 0 {
+		ctx.Errorf("Empty value for hit handler")
+		return
+	}
+	incHits(ctx, path)
+}

File appengine-demo/static/logo.png

Added
New image

File appengine-demo/static/style.css

+body {
+    margin: 20px;
+    border: 2px solid blue;
+    padding: 1px;
+}
+
+input[name=url] {
+    width: 80%;
+    margin-left: 4px;
+}
+
+span.right {
+    float: right;
+    margin-right: 5px;
+}
+
+div.header {
+    padding: 2px;
+    background: silver;
+    border-bottom: 1px solid gray;
+    margin-bottom: 10px;
+}
+
+table.urls {
+    border-collapse: collapse;
+    width: 100%;
+    border: 1px dashed blue;
+}
+
+table.urls th {
+    text-align: left;
+}
+
+div.footer {
+    margin-top: 10px;
+    padding-top: 5px;
+    border-top: 1px dashed gray;
+    padding-left: 10px;
+    font-family: Monospace;
+}
+
+div.error {
+    color: red;
+}
+
+tr.even {
+    background: #DCDCDC;
+}