// Copyright 2012 The Go Authors. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. package build import ( "net/http" "regexp" "strings" "appengine" "appengine/datastore" "appengine/delay" "appengine/urlfetch" ) func init() { http.HandleFunc("/install", installHandler) http.HandleFunc("/install/cron", installCronHandler) } // installHandler serves requests from the go tool to increment the install // count for a given package. func installHandler(w http.ResponseWriter, r *http.Request) { installLater.Call(appengine.NewContext(r), r.FormValue("packagePath")) } // installCronHandler starts a task to update the weekly install counts for // every external package. func installCronHandler(w http.ResponseWriter, r *http.Request) { c := appengine.NewContext(r) q := datastore.NewQuery("Package").Filter("Kind=", "external").KeysOnly() for t := q.Run(c); ; { key, err := t.Next(nil) if err == datastore.Done { break } else if err != nil { c.Errorf("%v", err) return } updateWeeklyLater.Call(c, key) } } var ( installLater = delay.Func("install", install) updateWeeklyLater = delay.Func("updateWeekly", updateWeekly) ) // install validates the provided package path, increments its install count, // and creates the Package record if it doesn't exist. func install(c appengine.Context, path string) { if !validPath(c, path) { return } tx := func(c appengine.Context) error { p := &Package{Path: path, Kind: "external"} err := datastore.Get(c, p.Key(c), p) if err != nil && err != datastore.ErrNoSuchEntity { return err } p.IncrementInstalls() _, err = datastore.Put(c, p.Key(c), p) return err } if err := datastore.RunInTransaction(c, tx, nil); err != nil { c.Errorf("install(%q): %v", path, err) } } // updateWeekly updates the weekly count for the specified Package. func updateWeekly(c appengine.Context, key *datastore.Key) { tx := func(c appengine.Context) error { p := new(Package) if err := datastore.Get(c, key, p); err != nil { return err } p.UpdateInstallsThisWeek() _, err := datastore.Put(c, key, p) return err } if err := datastore.RunInTransaction(c, tx, nil); err != nil { c.Errorf("updateWeekly: %v", err) } } // validPath validates the specified import path by matching it against the // vcsPath regexen and validating its existence by making an HTTP GET request // to the remote repository. func validPath(c appengine.Context, path string) bool { for _, p := range vcsPaths { if !strings.HasPrefix(path, p.prefix) { continue } m := p.regexp.FindStringSubmatch(path) if m == nil { continue } if p.check == nil { // no check function, so just say OK return true } match := make(map[string]string) for i, name := range p.regexp.SubexpNames() { if name != "" { match[name] = m[i] } } return p.check(c, match) } c.Debugf("validPath(%q): matching vcsPath not found", path) return false } // A vcsPath describes how to convert an import path into a version control // system and repository name. // // This is a cut down and modified version of the data structure from // $GOROOT/src/cmd/go/vcs.go. type vcsPath struct { prefix string // prefix this description applies to re string // pattern for import path // check should perform an HTTP request to validate the import path check func(c appengine.Context, match map[string]string) bool regexp *regexp.Regexp // cached compiled form of re } // vcsPaths lists the known vcs paths. // // This is a cut down version of the data from $GOROOT/src/cmd/go/vcs.go. var vcsPaths = []*vcsPath{ // Google Code - new syntax { prefix: "code.google.com/", re: `^(?Pcode\.google\.com/p/(?P[a-z0-9\-]+)(\.(?P[a-z0-9\-]+))?)(/[A-Za-z0-9_.\-]+)*$`, check: googleCodeVCS, }, // Github { prefix: "github.com/", re: `^(?Pgithub\.com/[A-Za-z0-9_.\-]+/[A-Za-z0-9_.\-]+)(/[A-Za-z0-9_.\-]+)*$`, check: checkRoot, }, // Bitbucket { prefix: "bitbucket.org/", re: `^(?Pbitbucket\.org/(?P[A-Za-z0-9_.\-]+/[A-Za-z0-9_.\-]+))(/[A-Za-z0-9_.\-]+)*$`, check: checkRoot, }, // Launchpad { prefix: "launchpad.net/", re: `^(?Plaunchpad\.net/((?P[A-Za-z0-9_.\-]+)(?P/[A-Za-z0-9_.\-]+)?|~[A-Za-z0-9_.\-]+/(\+junk|[A-Za-z0-9_.\-]+)/[A-Za-z0-9_.\-]+))(/[A-Za-z0-9_.\-]+)*$`, // TODO(adg): write check function for Launchpad }, } func init() { // Compile the regular expressions. for i := range vcsPaths { vcsPaths[i].regexp = regexp.MustCompile(vcsPaths[i].re) } } // googleCodeVCS performs an HTTP GET to verify that a Google Code project // (and, optionally, a sub-repository) exists. func googleCodeVCS(c appengine.Context, match map[string]string) bool { u := "https://code.google.com/p/" + match["project"] if match["subrepo"] != "" { u += "/source/checkout?repo=" + match["subrepo"] } return checkURL(c, u) } // checkRoot performs an HTTP GET to verify that a specific repository root // exists (for github and bitbucket both). func checkRoot(c appengine.Context, match map[string]string) bool { return checkURL(c, "https://"+match["root"]) } // checkURL performs an HTTP GET to the specified URL and returns whether the // remote server returned a 2xx response. func checkURL(c appengine.Context, u string) bool { client := urlfetch.Client(c) resp, err := client.Get(u) if err != nil { c.Errorf("checkURL(%q): %v", u, err) return false } if resp.StatusCode/100 != 2 { c.Debugf("checkURL(%q): HTTP status: %s", u, resp.Status) return false } return true }