mirror of
https://github.com/golang/go
synced 2024-11-19 00:54:42 -07:00
8b89e95de3
This change makes each release show only once on the whole page. The current stable releases are not shown under the archive. This change also fixes expandos to work correctly on IDs that are not valid CSS selectors (i.e., "#go1.7.4") to ensure that the file listing for a given version is expanded. Updates golang/go#17574. Change-Id: I7ff1041be3e0072286772ffa545a124764f81945 Reviewed-on: https://go-review.googlesource.com/34911 Reviewed-by: Brad Fitzpatrick <bradfitz@golang.org>
555 lines
13 KiB
Go
555 lines
13 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.
|
|
|
|
// +build appengine
|
|
|
|
// Package dl implements a simple downloads frontend server.
|
|
//
|
|
// It accepts HTTP POST requests to create a new download metadata entity, and
|
|
// lists entities with sorting and filtering.
|
|
// It is designed to run only on the instance of godoc that serves golang.org.
|
|
package dl
|
|
|
|
import (
|
|
"crypto/hmac"
|
|
"crypto/md5"
|
|
"encoding/json"
|
|
"fmt"
|
|
"html/template"
|
|
"io"
|
|
"net/http"
|
|
"regexp"
|
|
"sort"
|
|
"strconv"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
|
|
"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 (
|
|
gcsBaseURL = "https://storage.googleapis.com/golang/"
|
|
cacheKey = "download_list_3" // increment if listTemplateData changes
|
|
cacheDuration = time.Hour
|
|
)
|
|
|
|
func RegisterHandlers(mux *http.ServeMux) {
|
|
mux.Handle("/dl", http.RedirectHandler("/dl/", http.StatusFound))
|
|
mux.HandleFunc("/dl/", getHandler) // also serves listHandler
|
|
mux.HandleFunc("/dl/upload", uploadHandler)
|
|
mux.HandleFunc("/dl/init", initHandler)
|
|
}
|
|
|
|
type File struct {
|
|
Filename string
|
|
OS string
|
|
Arch string
|
|
Version string
|
|
Checksum string `datastore:",noindex"` // SHA1; deprecated
|
|
ChecksumSHA256 string `datastore:",noindex"`
|
|
Size int64 `datastore:",noindex"`
|
|
Kind string // "archive", "installer", "source"
|
|
Uploaded time.Time
|
|
}
|
|
|
|
func (f File) ChecksumType() string {
|
|
if f.ChecksumSHA256 != "" {
|
|
return "SHA256"
|
|
}
|
|
return "SHA1"
|
|
}
|
|
|
|
func (f File) PrettyChecksum() string {
|
|
if f.ChecksumSHA256 != "" {
|
|
return f.ChecksumSHA256
|
|
}
|
|
return f.Checksum
|
|
}
|
|
|
|
func (f File) PrettyOS() string {
|
|
if f.OS == "darwin" {
|
|
switch {
|
|
case strings.Contains(f.Filename, "osx10.8"):
|
|
return "OS X 10.8+"
|
|
case strings.Contains(f.Filename, "osx10.6"):
|
|
return "OS X 10.6+"
|
|
}
|
|
}
|
|
return pretty(f.OS)
|
|
}
|
|
|
|
func (f File) PrettySize() string {
|
|
const mb = 1 << 20
|
|
if f.Size == 0 {
|
|
return ""
|
|
}
|
|
if f.Size < mb {
|
|
// All Go releases are >1mb, but handle this case anyway.
|
|
return fmt.Sprintf("%v bytes", f.Size)
|
|
}
|
|
return fmt.Sprintf("%.0fMB", float64(f.Size)/mb)
|
|
}
|
|
|
|
var primaryPorts = map[string]bool{
|
|
"darwin/amd64": true,
|
|
"linux/386": true,
|
|
"linux/amd64": true,
|
|
"linux/armv6l": true,
|
|
"windows/386": true,
|
|
"windows/amd64": true,
|
|
}
|
|
|
|
func (f File) PrimaryPort() bool {
|
|
if f.Kind == "source" {
|
|
return true
|
|
}
|
|
return primaryPorts[f.OS+"/"+f.Arch]
|
|
}
|
|
|
|
func (f File) Highlight() bool {
|
|
switch {
|
|
case f.Kind == "source":
|
|
return true
|
|
case f.Arch == "amd64" && f.OS == "linux":
|
|
return true
|
|
case f.Arch == "amd64" && f.Kind == "installer":
|
|
switch f.OS {
|
|
case "windows":
|
|
return true
|
|
case "darwin":
|
|
if !strings.Contains(f.Filename, "osx10.6") {
|
|
return true
|
|
}
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
func (f File) URL() string {
|
|
return gcsBaseURL + f.Filename
|
|
}
|
|
|
|
type Release struct {
|
|
Version string
|
|
Stable bool
|
|
Files []File
|
|
Visible bool // show files on page load
|
|
SplitPortTable bool // whether files should be split by primary/other ports.
|
|
}
|
|
|
|
type Feature struct {
|
|
// The File field will be filled in by the first stable File
|
|
// whose name matches the given fileRE.
|
|
File
|
|
fileRE *regexp.Regexp
|
|
|
|
Platform string // "Microsoft Windows", "Mac OS X", "Linux"
|
|
Requirements string // "Windows XP and above, 64-bit Intel Processor"
|
|
}
|
|
|
|
// featuredFiles lists the platforms and files to be featured
|
|
// at the top of the downloads page.
|
|
var featuredFiles = []Feature{
|
|
{
|
|
Platform: "Microsoft Windows",
|
|
Requirements: "Windows XP or later, Intel 64-bit processor",
|
|
fileRE: regexp.MustCompile(`\.windows-amd64\.msi$`),
|
|
},
|
|
{
|
|
Platform: "Apple OS X",
|
|
Requirements: "OS X 10.8 or later, Intel 64-bit processor",
|
|
fileRE: regexp.MustCompile(`\.darwin-amd64(-osx10\.8)?\.pkg$`),
|
|
},
|
|
{
|
|
Platform: "Linux",
|
|
Requirements: "Linux 2.6.23 or later, Intel 64-bit processor",
|
|
fileRE: regexp.MustCompile(`\.linux-amd64\.tar\.gz$`),
|
|
},
|
|
{
|
|
Platform: "Source",
|
|
fileRE: regexp.MustCompile(`\.src\.tar\.gz$`),
|
|
},
|
|
}
|
|
|
|
// data to send to the template; increment cacheKey if you change this.
|
|
type listTemplateData struct {
|
|
Featured []Feature
|
|
Stable, Unstable, Archive []Release
|
|
LoginURL string
|
|
}
|
|
|
|
var (
|
|
listTemplate = template.Must(template.New("").Funcs(templateFuncs).Parse(templateHTML))
|
|
templateFuncs = template.FuncMap{"pretty": pretty}
|
|
)
|
|
|
|
func listHandler(w http.ResponseWriter, r *http.Request) {
|
|
if r.Method != "GET" {
|
|
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
|
|
return
|
|
}
|
|
var (
|
|
c = appengine.NewContext(r)
|
|
d listTemplateData
|
|
)
|
|
if _, err := memcache.Gob.Get(c, cacheKey, &d); err != nil {
|
|
if err == memcache.ErrCacheMiss {
|
|
log.Debugf(c, "cache miss")
|
|
} else {
|
|
log.Errorf(c, "cache get error: %v", err)
|
|
}
|
|
|
|
var fs []File
|
|
_, err := datastore.NewQuery("File").Ancestor(rootKey(c)).GetAll(c, &fs)
|
|
if err != nil {
|
|
log.Errorf(c, "error listing: %v", err)
|
|
return
|
|
}
|
|
d.Stable, d.Unstable, d.Archive = filesToReleases(fs)
|
|
if len(d.Stable) > 0 {
|
|
d.Featured = filesToFeatured(d.Stable[0].Files)
|
|
}
|
|
|
|
d.LoginURL, _ = user.LoginURL(c, "/dl")
|
|
if user.Current(c) != nil {
|
|
d.LoginURL, _ = user.LogoutURL(c, "/dl")
|
|
}
|
|
|
|
item := &memcache.Item{Key: cacheKey, Object: &d, Expiration: cacheDuration}
|
|
if err := memcache.Gob.Set(c, item); err != nil {
|
|
log.Errorf(c, "cache set error: %v", err)
|
|
}
|
|
}
|
|
if err := listTemplate.ExecuteTemplate(w, "root", d); err != nil {
|
|
log.Errorf(c, "error executing template: %v", err)
|
|
}
|
|
}
|
|
|
|
func filesToFeatured(fs []File) (featured []Feature) {
|
|
for _, feature := range featuredFiles {
|
|
for _, file := range fs {
|
|
if feature.fileRE.MatchString(file.Filename) {
|
|
feature.File = file
|
|
featured = append(featured, feature)
|
|
break
|
|
}
|
|
}
|
|
}
|
|
return
|
|
}
|
|
|
|
func filesToReleases(fs []File) (stable, unstable, archive []Release) {
|
|
sort.Sort(fileOrder(fs))
|
|
|
|
var r *Release
|
|
var stableMaj, stableMin int
|
|
add := func() {
|
|
if r == nil {
|
|
return
|
|
}
|
|
if !r.Stable {
|
|
if len(unstable) != 0 {
|
|
// Only show one (latest) unstable version.
|
|
return
|
|
}
|
|
maj, min, _ := parseVersion(r.Version)
|
|
if maj < stableMaj || maj == stableMaj && min <= stableMin {
|
|
// Display unstable version only if newer than the
|
|
// latest stable release.
|
|
return
|
|
}
|
|
unstable = append(unstable, *r)
|
|
}
|
|
|
|
// Reports whether the release is the most recent minor version of the
|
|
// two most recent major versions.
|
|
shouldAddStable := func() bool {
|
|
if len(stable) >= 2 {
|
|
// Show up to two stable versions.
|
|
return false
|
|
}
|
|
if len(stable) == 0 {
|
|
// Most recent stable version.
|
|
stableMaj, stableMin, _ = parseVersion(r.Version)
|
|
return true
|
|
}
|
|
if maj, _, _ := parseVersion(r.Version); maj == stableMaj {
|
|
// Older minor version of most recent major version.
|
|
return false
|
|
}
|
|
// Second most recent stable version.
|
|
return true
|
|
}
|
|
if !shouldAddStable() {
|
|
archive = append(archive, *r)
|
|
return
|
|
}
|
|
|
|
// Split the file list into primary/other ports for the stable releases.
|
|
// NOTE(cbro): This is only done for stable releases because maintaining the historical
|
|
// nature of primary/other ports for older versions is infeasible.
|
|
// If freebsd is considered primary some time in the future, we'd not want to
|
|
// mark all of the older freebsd binaries as "primary".
|
|
// It might be better if we set that as a flag when uploading.
|
|
r.SplitPortTable = true
|
|
r.Visible = true // Toggle open all stable releases.
|
|
stable = append(stable, *r)
|
|
}
|
|
for _, f := range fs {
|
|
if r == nil || f.Version != r.Version {
|
|
add()
|
|
r = &Release{
|
|
Version: f.Version,
|
|
Stable: isStable(f.Version),
|
|
}
|
|
}
|
|
r.Files = append(r.Files, f)
|
|
}
|
|
add()
|
|
return
|
|
}
|
|
|
|
// isStable reports whether the version string v is a stable version.
|
|
func isStable(v string) bool {
|
|
return !strings.Contains(v, "beta") && !strings.Contains(v, "rc")
|
|
}
|
|
|
|
type fileOrder []File
|
|
|
|
func (s fileOrder) Len() int { return len(s) }
|
|
func (s fileOrder) Swap(i, j int) { s[i], s[j] = s[j], s[i] }
|
|
func (s fileOrder) Less(i, j int) bool {
|
|
a, b := s[i], s[j]
|
|
if av, bv := a.Version, b.Version; av != bv {
|
|
return versionLess(av, bv)
|
|
}
|
|
if a.OS != b.OS {
|
|
return a.OS < b.OS
|
|
}
|
|
if a.Arch != b.Arch {
|
|
return a.Arch < b.Arch
|
|
}
|
|
if a.Kind != b.Kind {
|
|
return a.Kind < b.Kind
|
|
}
|
|
return a.Filename < b.Filename
|
|
}
|
|
|
|
func versionLess(a, b string) bool {
|
|
// Put stable releases first.
|
|
if isStable(a) != isStable(b) {
|
|
return isStable(a)
|
|
}
|
|
maja, mina, ta := parseVersion(a)
|
|
majb, minb, tb := parseVersion(b)
|
|
if maja == majb {
|
|
if mina == minb {
|
|
return ta >= tb
|
|
}
|
|
return mina >= minb
|
|
}
|
|
return maja >= majb
|
|
}
|
|
|
|
func parseVersion(v string) (maj, min int, tail string) {
|
|
if i := strings.Index(v, "beta"); i > 0 {
|
|
tail = v[i:]
|
|
v = v[:i]
|
|
}
|
|
if i := strings.Index(v, "rc"); i > 0 {
|
|
tail = v[i:]
|
|
v = v[:i]
|
|
}
|
|
p := strings.Split(strings.TrimPrefix(v, "go1."), ".")
|
|
maj, _ = strconv.Atoi(p[0])
|
|
if len(p) < 2 {
|
|
return
|
|
}
|
|
min, _ = strconv.Atoi(p[1])
|
|
return
|
|
}
|
|
|
|
func uploadHandler(w http.ResponseWriter, r *http.Request) {
|
|
if r.Method != "POST" {
|
|
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
|
|
return
|
|
}
|
|
c := appengine.NewContext(r)
|
|
|
|
// Authenticate using a user token (same as gomote).
|
|
user := r.FormValue("user")
|
|
if !validUser(user) {
|
|
http.Error(w, "bad user", http.StatusForbidden)
|
|
return
|
|
}
|
|
if r.FormValue("key") != userKey(c, user) {
|
|
http.Error(w, "bad key", http.StatusForbidden)
|
|
return
|
|
}
|
|
|
|
var f File
|
|
defer r.Body.Close()
|
|
if err := json.NewDecoder(r.Body).Decode(&f); err != nil {
|
|
log.Errorf(c, "error decoding upload JSON: %v", err)
|
|
http.Error(w, "Something broke", http.StatusInternalServerError)
|
|
return
|
|
}
|
|
if f.Filename == "" {
|
|
http.Error(w, "Must provide Filename", http.StatusBadRequest)
|
|
return
|
|
}
|
|
if f.Uploaded.IsZero() {
|
|
f.Uploaded = time.Now()
|
|
}
|
|
k := datastore.NewKey(c, "File", f.Filename, 0, rootKey(c))
|
|
if _, err := datastore.Put(c, k, &f); err != nil {
|
|
log.Errorf(c, "putting File entity: %v", err)
|
|
http.Error(w, "could not put File entity", http.StatusInternalServerError)
|
|
return
|
|
}
|
|
if err := memcache.Delete(c, cacheKey); err != nil {
|
|
log.Errorf(c, "cache delete error: %v", err)
|
|
}
|
|
io.WriteString(w, "OK")
|
|
}
|
|
|
|
func getHandler(w http.ResponseWriter, r *http.Request) {
|
|
name := strings.TrimPrefix(r.URL.Path, "/dl/")
|
|
if name == "" {
|
|
listHandler(w, r)
|
|
return
|
|
}
|
|
if !fileRe.MatchString(name) {
|
|
http.NotFound(w, r)
|
|
return
|
|
}
|
|
http.Redirect(w, r, gcsBaseURL+name, http.StatusFound)
|
|
}
|
|
|
|
func validUser(user string) bool {
|
|
switch user {
|
|
case "adg", "bradfitz", "cbro":
|
|
return true
|
|
}
|
|
return false
|
|
}
|
|
|
|
func userKey(c context.Context, user string) string {
|
|
h := hmac.New(md5.New, []byte(secret(c)))
|
|
h.Write([]byte("user-" + user))
|
|
return fmt.Sprintf("%x", h.Sum(nil))
|
|
}
|
|
|
|
var fileRe = regexp.MustCompile(`^go[0-9a-z.]+\.[0-9a-z.-]+\.(tar\.gz|pkg|msi|zip)$`)
|
|
|
|
func initHandler(w http.ResponseWriter, r *http.Request) {
|
|
var fileRoot struct {
|
|
Root string
|
|
}
|
|
c := appengine.NewContext(r)
|
|
k := rootKey(c)
|
|
err := datastore.RunInTransaction(c, func(c context.Context) error {
|
|
err := datastore.Get(c, k, &fileRoot)
|
|
if err != nil && err != datastore.ErrNoSuchEntity {
|
|
return err
|
|
}
|
|
_, err = datastore.Put(c, k, &fileRoot)
|
|
return err
|
|
}, nil)
|
|
if err != nil {
|
|
http.Error(w, err.Error(), 500)
|
|
return
|
|
}
|
|
io.WriteString(w, "OK")
|
|
}
|
|
|
|
// rootKey is the ancestor of all File entities.
|
|
func rootKey(c context.Context) *datastore.Key {
|
|
return datastore.NewKey(c, "FileRoot", "root", 0, nil)
|
|
}
|
|
|
|
// pretty returns a human-readable version of the given OS, Arch, or Kind.
|
|
func pretty(s string) string {
|
|
t, ok := prettyStrings[s]
|
|
if !ok {
|
|
return s
|
|
}
|
|
return t
|
|
}
|
|
|
|
var prettyStrings = map[string]string{
|
|
"darwin": "OS X",
|
|
"freebsd": "FreeBSD",
|
|
"linux": "Linux",
|
|
"windows": "Windows",
|
|
|
|
"386": "32-bit",
|
|
"amd64": "64-bit",
|
|
|
|
"armv6l": "ARMv6",
|
|
|
|
"archive": "Archive",
|
|
"installer": "Installer",
|
|
"source": "Source",
|
|
}
|
|
|
|
// Code below copied from x/build/app/key
|
|
|
|
var theKey struct {
|
|
sync.RWMutex
|
|
builderKey
|
|
}
|
|
|
|
type builderKey struct {
|
|
Secret string
|
|
}
|
|
|
|
func (k *builderKey) Key(c context.Context) *datastore.Key {
|
|
return datastore.NewKey(c, "BuilderKey", "root", 0, nil)
|
|
}
|
|
|
|
func secret(c context.Context) string {
|
|
// check with rlock
|
|
theKey.RLock()
|
|
k := theKey.Secret
|
|
theKey.RUnlock()
|
|
if k != "" {
|
|
return k
|
|
}
|
|
|
|
// prepare to fill; check with lock and keep lock
|
|
theKey.Lock()
|
|
defer theKey.Unlock()
|
|
if theKey.Secret != "" {
|
|
return theKey.Secret
|
|
}
|
|
|
|
// fill
|
|
if err := datastore.Get(c, theKey.Key(c), &theKey.builderKey); err != nil {
|
|
if err == datastore.ErrNoSuchEntity {
|
|
// If the key is not stored in datastore, write it.
|
|
// This only happens at the beginning of a new deployment.
|
|
// The code is left here for SDK use and in case a fresh
|
|
// deployment is ever needed. "gophers rule" is not the
|
|
// real key.
|
|
if !appengine.IsDevAppServer() {
|
|
panic("lost key from datastore")
|
|
}
|
|
theKey.Secret = "gophers rule"
|
|
datastore.Put(c, theKey.Key(c), &theKey.builderKey)
|
|
return theKey.Secret
|
|
}
|
|
panic("cannot load builder key: " + err.Error())
|
|
}
|
|
|
|
return theKey.Secret
|
|
}
|