mirror of
https://github.com/golang/go
synced 2024-11-19 04:14:45 -07:00
e83bc56334
These were built inside Google but have been in production for years. Move them into the public tools repository so that they can be more easily maintained. This is step one to moving the entire golang.org deployment process out of Google's internal source repository. Change-Id: I72f875c5020b3f58f1c0cea1d19268e0f991833f Reviewed-on: https://go-review.googlesource.com/14842 Reviewed-by: Chris Broadfoot <cbro@golang.org>
172 lines
4.3 KiB
Go
172 lines
4.3 KiB
Go
// Copyright 2015 The Go Authors. All rights reserved.
|
|
// Use of this source code is governed by the Apache 2.0
|
|
// license that can be found in the LICENSE file.
|
|
|
|
// Package short implements a simple URL shortener, serving an administrative
|
|
// interface at /s and shortened urls from /s/key.
|
|
// It is designed to run only on the instance of godoc that serves golang.org.
|
|
package short
|
|
|
|
// TODO(adg): collect statistics on URL visits
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"html/template"
|
|
"net/http"
|
|
"net/url"
|
|
"regexp"
|
|
|
|
"golang.org/x/net/context"
|
|
|
|
"google.golang.org/appengine"
|
|
"google.golang.org/appengine/datastore"
|
|
"google.golang.org/appengine/log"
|
|
"google.golang.org/appengine/memcache"
|
|
"google.golang.org/appengine/user"
|
|
)
|
|
|
|
const (
|
|
prefix = "/s"
|
|
kind = "Link"
|
|
baseURL = "https://golang.org" + prefix
|
|
)
|
|
|
|
// Link represents a short link.
|
|
type Link struct {
|
|
Key, Target string
|
|
}
|
|
|
|
var validKey = regexp.MustCompile(`^[a-zA-Z0-9-_.]+$`)
|
|
|
|
func RegisterHandlers(mux *http.ServeMux) {
|
|
mux.HandleFunc(prefix, adminHandler)
|
|
mux.HandleFunc(prefix+"/", linkHandler)
|
|
}
|
|
|
|
// linkHandler services requests to short URLs.
|
|
// http://golang.org/s/key
|
|
// It consults memcache and datastore for the Link for key.
|
|
// It then sends a redirects or an error message.
|
|
func linkHandler(w http.ResponseWriter, r *http.Request) {
|
|
c := appengine.NewContext(r)
|
|
|
|
key := r.URL.Path[len(prefix)+1:]
|
|
if !validKey.MatchString(key) {
|
|
http.Error(w, "not found", http.StatusNotFound)
|
|
return
|
|
}
|
|
|
|
var link Link
|
|
_, err := memcache.JSON.Get(c, cacheKey(key), &link)
|
|
if err != nil {
|
|
k := datastore.NewKey(c, kind, key, 0, nil)
|
|
err = datastore.Get(c, k, &link)
|
|
switch err {
|
|
case datastore.ErrNoSuchEntity:
|
|
http.Error(w, "not found", http.StatusNotFound)
|
|
return
|
|
default: // != nil
|
|
log.Errorf(c, "%q: %v", key, err)
|
|
http.Error(w, "internal server error", http.StatusInternalServerError)
|
|
return
|
|
case nil:
|
|
item := &memcache.Item{
|
|
Key: cacheKey(key),
|
|
Object: &link,
|
|
}
|
|
if err := memcache.JSON.Set(c, item); err != nil {
|
|
log.Warningf(c, "%q: %v", key, err)
|
|
}
|
|
}
|
|
}
|
|
|
|
http.Redirect(w, r, link.Target, http.StatusFound)
|
|
}
|
|
|
|
var adminTemplate = template.Must(template.New("admin").Parse(templateHTML))
|
|
|
|
// adminHandler serves an administrative interface.
|
|
func adminHandler(w http.ResponseWriter, r *http.Request) {
|
|
c := appengine.NewContext(r)
|
|
|
|
if !user.IsAdmin(c) {
|
|
http.Error(w, "forbidden", http.StatusForbidden)
|
|
return
|
|
}
|
|
|
|
var newLink *Link
|
|
var doErr error
|
|
if r.Method == "POST" {
|
|
key := r.FormValue("key")
|
|
switch r.FormValue("do") {
|
|
case "Add":
|
|
newLink = &Link{key, r.FormValue("target")}
|
|
doErr = putLink(c, newLink)
|
|
case "Delete":
|
|
k := datastore.NewKey(c, kind, key, 0, nil)
|
|
doErr = datastore.Delete(c, k)
|
|
default:
|
|
http.Error(w, "unknown action", http.StatusBadRequest)
|
|
}
|
|
err := memcache.Delete(c, cacheKey(key))
|
|
if err != nil && err != memcache.ErrCacheMiss {
|
|
log.Warningf(c, "%q: %v", key, err)
|
|
}
|
|
}
|
|
|
|
var links []*Link
|
|
_, err := datastore.NewQuery(kind).Order("Key").GetAll(c, &links)
|
|
if err != nil {
|
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
log.Errorf(c, "%v", err)
|
|
return
|
|
}
|
|
|
|
// Put the new link in the list if it's not there already.
|
|
// (Eventual consistency means that it might not show up
|
|
// immediately, which might be confusing for the user.)
|
|
if newLink != nil && doErr == nil {
|
|
found := false
|
|
for i := range links {
|
|
if links[i].Key == newLink.Key {
|
|
found = true
|
|
break
|
|
}
|
|
}
|
|
if !found {
|
|
links = append([]*Link{newLink}, links...)
|
|
}
|
|
newLink = nil
|
|
}
|
|
|
|
var data = struct {
|
|
BaseURL string
|
|
Prefix string
|
|
Links []*Link
|
|
New *Link
|
|
Error error
|
|
}{baseURL, prefix, links, newLink, doErr}
|
|
if err := adminTemplate.Execute(w, &data); err != nil {
|
|
log.Criticalf(c, "adminTemplate: %v", err)
|
|
}
|
|
}
|
|
|
|
// putLink validates the provided link and puts it into the datastore.
|
|
func putLink(c context.Context, link *Link) error {
|
|
if !validKey.MatchString(link.Key) {
|
|
return errors.New("invalid key; must match " + validKey.String())
|
|
}
|
|
if _, err := url.Parse(link.Target); err != nil {
|
|
return fmt.Errorf("bad target: %v", err)
|
|
}
|
|
k := datastore.NewKey(c, kind, link.Key, 0, nil)
|
|
_, err := datastore.Put(c, k, link)
|
|
return err
|
|
}
|
|
|
|
// cacheKey returns a short URL key as a memcache key.
|
|
func cacheKey(key string) string {
|
|
return "link-" + key
|
|
}
|