mirror of
https://github.com/dutchcoders/transfer.sh.git
synced 2024-12-05 02:10:18 +01:00
cb6e5cb0c7
* use dep for vendoring * lets encrypt * moved web to transfer.sh-web repo * single command install * added first tests
575 lines
15 KiB
Go
575 lines
15 KiB
Go
// Copyright 2013 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 or at
|
|
// https://developers.google.com/open-source/licenses/bsd.
|
|
|
|
package main
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/base64"
|
|
"errors"
|
|
"fmt"
|
|
godoc "go/doc"
|
|
htemp "html/template"
|
|
"io"
|
|
"net/http"
|
|
"net/url"
|
|
"path"
|
|
"path/filepath"
|
|
"reflect"
|
|
"regexp"
|
|
"sort"
|
|
"strings"
|
|
ttemp "text/template"
|
|
"time"
|
|
|
|
"github.com/spf13/viper"
|
|
|
|
"github.com/golang/gddo/doc"
|
|
"github.com/golang/gddo/gosrc"
|
|
"github.com/golang/gddo/httputil"
|
|
)
|
|
|
|
var cacheBusters httputil.CacheBusters
|
|
|
|
type flashMessage struct {
|
|
ID string
|
|
Args []string
|
|
}
|
|
|
|
// getFlashMessages retrieves flash messages from the request and clears the flash cookie if needed.
|
|
func getFlashMessages(resp http.ResponseWriter, req *http.Request) []flashMessage {
|
|
c, err := req.Cookie("flash")
|
|
if err == http.ErrNoCookie {
|
|
return nil
|
|
}
|
|
http.SetCookie(resp, &http.Cookie{Name: "flash", Path: "/", MaxAge: -1, Expires: time.Now().Add(-100 * 24 * time.Hour)})
|
|
if err != nil {
|
|
return nil
|
|
}
|
|
p, err := base64.URLEncoding.DecodeString(c.Value)
|
|
if err != nil {
|
|
return nil
|
|
}
|
|
var messages []flashMessage
|
|
for _, s := range strings.Split(string(p), "\000") {
|
|
idArgs := strings.Split(s, "\001")
|
|
messages = append(messages, flashMessage{ID: idArgs[0], Args: idArgs[1:]})
|
|
}
|
|
return messages
|
|
}
|
|
|
|
// setFlashMessages sets a cookie with the given flash messages.
|
|
func setFlashMessages(resp http.ResponseWriter, messages []flashMessage) {
|
|
var buf []byte
|
|
for i, message := range messages {
|
|
if i > 0 {
|
|
buf = append(buf, '\000')
|
|
}
|
|
buf = append(buf, message.ID...)
|
|
for _, arg := range message.Args {
|
|
buf = append(buf, '\001')
|
|
buf = append(buf, arg...)
|
|
}
|
|
}
|
|
value := base64.URLEncoding.EncodeToString(buf)
|
|
http.SetCookie(resp, &http.Cookie{Name: "flash", Value: value, Path: "/"})
|
|
}
|
|
|
|
type tdoc struct {
|
|
*doc.Package
|
|
allExamples []*texample
|
|
}
|
|
|
|
type texample struct {
|
|
ID string
|
|
Label string
|
|
Example *doc.Example
|
|
Play bool
|
|
obj interface{}
|
|
}
|
|
|
|
func newTDoc(pdoc *doc.Package) *tdoc {
|
|
return &tdoc{Package: pdoc}
|
|
}
|
|
|
|
func (pdoc *tdoc) SourceLink(pos doc.Pos, text string, textOnlyOK bool) htemp.HTML {
|
|
if pos.Line == 0 || pdoc.LineFmt == "" || pdoc.Files[pos.File].URL == "" {
|
|
if textOnlyOK {
|
|
return htemp.HTML(htemp.HTMLEscapeString(text))
|
|
}
|
|
return ""
|
|
}
|
|
return htemp.HTML(fmt.Sprintf(`<a title="View Source" href="%s">%s</a>`,
|
|
htemp.HTMLEscapeString(fmt.Sprintf(pdoc.LineFmt, pdoc.Files[pos.File].URL, pos.Line)),
|
|
htemp.HTMLEscapeString(text)))
|
|
}
|
|
|
|
// UsesLink generates a link to uses of a symbol definition.
|
|
// title is used as the tooltip. defParts are parts of the symbol definition name.
|
|
func (pdoc *tdoc) UsesLink(title string, defParts ...string) htemp.HTML {
|
|
if viper.GetString(ConfigSourcegraphURL) == "" {
|
|
return ""
|
|
}
|
|
|
|
var def string
|
|
switch len(defParts) {
|
|
case 1:
|
|
// Funcs and types have one def part.
|
|
def = defParts[0]
|
|
|
|
case 3:
|
|
// Methods have three def parts, the original receiver name, actual receiver name and method name.
|
|
orig, recv, methodName := defParts[0], defParts[1], defParts[2]
|
|
|
|
if orig == "" {
|
|
// TODO: Remove this fallback after 2016-08-05. It's only needed temporarily to backfill data.
|
|
// Actual receiver is not needed, it's only used because original receiver value
|
|
// was recently added to gddo/doc package and will be blank until next package rebuild.
|
|
//
|
|
// Use actual receiver as fallback.
|
|
orig = recv
|
|
}
|
|
|
|
// Trim "*" from "*T" if it's a pointer receiver method.
|
|
typeName := strings.TrimPrefix(orig, "*")
|
|
|
|
def = typeName + "/" + methodName
|
|
default:
|
|
panic(fmt.Errorf("%v defParts, want 1 or 3", len(defParts)))
|
|
}
|
|
|
|
q := url.Values{
|
|
"repo": {pdoc.ProjectRoot},
|
|
"pkg": {pdoc.ImportPath},
|
|
"def": {def},
|
|
}
|
|
u := viper.GetString(ConfigSourcegraphURL) + "/-/godoc/refs?" + q.Encode()
|
|
return htemp.HTML(fmt.Sprintf(`<a class="uses" title="%s" href="%s">Uses</a>`, htemp.HTMLEscapeString(title), htemp.HTMLEscapeString(u)))
|
|
}
|
|
|
|
func (pdoc *tdoc) PageName() string {
|
|
if pdoc.Name != "" && !pdoc.IsCmd {
|
|
return pdoc.Name
|
|
}
|
|
_, name := path.Split(pdoc.ImportPath)
|
|
return name
|
|
}
|
|
|
|
func (pdoc *tdoc) addExamples(obj interface{}, export, method string, examples []*doc.Example) {
|
|
label := export
|
|
id := export
|
|
if method != "" {
|
|
label += "." + method
|
|
id += "-" + method
|
|
}
|
|
for _, e := range examples {
|
|
te := &texample{
|
|
Label: label,
|
|
ID: id,
|
|
Example: e,
|
|
obj: obj,
|
|
// Only show play links for packages within the standard library.
|
|
Play: e.Play != "" && gosrc.IsGoRepoPath(pdoc.ImportPath),
|
|
}
|
|
if e.Name != "" {
|
|
te.Label += " (" + e.Name + ")"
|
|
if method == "" {
|
|
te.ID += "-"
|
|
}
|
|
te.ID += "-" + e.Name
|
|
}
|
|
pdoc.allExamples = append(pdoc.allExamples, te)
|
|
}
|
|
}
|
|
|
|
type byExampleID []*texample
|
|
|
|
func (e byExampleID) Len() int { return len(e) }
|
|
func (e byExampleID) Less(i, j int) bool { return e[i].ID < e[j].ID }
|
|
func (e byExampleID) Swap(i, j int) { e[i], e[j] = e[j], e[i] }
|
|
|
|
func (pdoc *tdoc) AllExamples() []*texample {
|
|
if pdoc.allExamples != nil {
|
|
return pdoc.allExamples
|
|
}
|
|
pdoc.allExamples = make([]*texample, 0)
|
|
pdoc.addExamples(pdoc, "package", "", pdoc.Examples)
|
|
for _, f := range pdoc.Funcs {
|
|
pdoc.addExamples(f, f.Name, "", f.Examples)
|
|
}
|
|
for _, t := range pdoc.Types {
|
|
pdoc.addExamples(t, t.Name, "", t.Examples)
|
|
for _, f := range t.Funcs {
|
|
pdoc.addExamples(f, f.Name, "", f.Examples)
|
|
}
|
|
for _, m := range t.Methods {
|
|
if len(m.Examples) > 0 {
|
|
pdoc.addExamples(m, t.Name, m.Name, m.Examples)
|
|
}
|
|
}
|
|
}
|
|
sort.Sort(byExampleID(pdoc.allExamples))
|
|
return pdoc.allExamples
|
|
}
|
|
|
|
func (pdoc *tdoc) ObjExamples(obj interface{}) []*texample {
|
|
var examples []*texample
|
|
for _, e := range pdoc.allExamples {
|
|
if e.obj == obj {
|
|
examples = append(examples, e)
|
|
}
|
|
}
|
|
return examples
|
|
}
|
|
|
|
func (pdoc *tdoc) Breadcrumbs(templateName string) htemp.HTML {
|
|
if !strings.HasPrefix(pdoc.ImportPath, pdoc.ProjectRoot) {
|
|
return ""
|
|
}
|
|
var buf bytes.Buffer
|
|
i := 0
|
|
j := len(pdoc.ProjectRoot)
|
|
if j == 0 {
|
|
j = strings.IndexRune(pdoc.ImportPath, '/')
|
|
if j < 0 {
|
|
j = len(pdoc.ImportPath)
|
|
}
|
|
}
|
|
for {
|
|
if i != 0 {
|
|
buf.WriteString(`<span class="text-muted">/</span>`)
|
|
}
|
|
link := j < len(pdoc.ImportPath) ||
|
|
(templateName != "dir.html" && templateName != "cmd.html" && templateName != "pkg.html")
|
|
if link {
|
|
buf.WriteString(`<a href="`)
|
|
buf.WriteString(formatPathFrag(pdoc.ImportPath[:j], ""))
|
|
buf.WriteString(`">`)
|
|
} else {
|
|
buf.WriteString(`<span class="text-muted">`)
|
|
}
|
|
buf.WriteString(htemp.HTMLEscapeString(pdoc.ImportPath[i:j]))
|
|
if link {
|
|
buf.WriteString("</a>")
|
|
} else {
|
|
buf.WriteString("</span>")
|
|
}
|
|
i = j + 1
|
|
if i >= len(pdoc.ImportPath) {
|
|
break
|
|
}
|
|
j = strings.IndexRune(pdoc.ImportPath[i:], '/')
|
|
if j < 0 {
|
|
j = len(pdoc.ImportPath)
|
|
} else {
|
|
j += i
|
|
}
|
|
}
|
|
return htemp.HTML(buf.String())
|
|
}
|
|
|
|
func (pdoc *tdoc) StatusDescription() htemp.HTML {
|
|
desc := ""
|
|
switch pdoc.Package.Status {
|
|
case gosrc.DeadEndFork:
|
|
desc = "This is a dead-end fork (no commits since the fork)."
|
|
case gosrc.QuickFork:
|
|
desc = "This is a quick bug-fix fork (has fewer than three commits, and only during the week it was created)."
|
|
case gosrc.Inactive:
|
|
desc = "This is an inactive package (no imports and no commits in at least two years)."
|
|
}
|
|
return htemp.HTML(desc)
|
|
}
|
|
|
|
func formatPathFrag(path, fragment string) string {
|
|
if len(path) > 0 && path[0] != '/' {
|
|
path = "/" + path
|
|
}
|
|
u := url.URL{Path: path, Fragment: fragment}
|
|
return u.String()
|
|
}
|
|
|
|
func hostFn(urlStr string) string {
|
|
u, err := url.Parse(urlStr)
|
|
if err != nil {
|
|
return ""
|
|
}
|
|
return u.Host
|
|
}
|
|
|
|
func mapFn(kvs ...interface{}) (map[string]interface{}, error) {
|
|
if len(kvs)%2 != 0 {
|
|
return nil, errors.New("map requires even number of arguments")
|
|
}
|
|
m := make(map[string]interface{})
|
|
for i := 0; i < len(kvs); i += 2 {
|
|
s, ok := kvs[i].(string)
|
|
if !ok {
|
|
return nil, errors.New("even args to map must be strings")
|
|
}
|
|
m[s] = kvs[i+1]
|
|
}
|
|
return m, nil
|
|
}
|
|
|
|
// relativePathFn formats an import path as HTML.
|
|
func relativePathFn(path string, parentPath interface{}) string {
|
|
if p, ok := parentPath.(string); ok && p != "" && strings.HasPrefix(path, p) {
|
|
path = path[len(p)+1:]
|
|
}
|
|
return path
|
|
}
|
|
|
|
// importPathFn formats an import with zero width space characters to allow for breaks.
|
|
func importPathFn(path string) htemp.HTML {
|
|
path = htemp.HTMLEscapeString(path)
|
|
if len(path) > 45 {
|
|
// Allow long import paths to break following "/"
|
|
path = strings.Replace(path, "/", "/​", -1)
|
|
}
|
|
return htemp.HTML(path)
|
|
}
|
|
|
|
var (
|
|
h3Pat = regexp.MustCompile(`<h3 id="([^"]+)">([^<]+)</h3>`)
|
|
rfcPat = regexp.MustCompile(`RFC\s+(\d{3,4})(,?\s+[Ss]ection\s+(\d+(\.\d+)*))?`)
|
|
packagePat = regexp.MustCompile(`\s+package\s+([-a-z0-9]\S+)`)
|
|
)
|
|
|
|
func replaceAll(src []byte, re *regexp.Regexp, replace func(out, src []byte, m []int) []byte) []byte {
|
|
var out []byte
|
|
for len(src) > 0 {
|
|
m := re.FindSubmatchIndex(src)
|
|
if m == nil {
|
|
break
|
|
}
|
|
out = append(out, src[:m[0]]...)
|
|
out = replace(out, src, m)
|
|
src = src[m[1]:]
|
|
}
|
|
if out == nil {
|
|
return src
|
|
}
|
|
return append(out, src...)
|
|
}
|
|
|
|
// commentFn formats a source code comment as HTML.
|
|
func commentFn(v string) htemp.HTML {
|
|
var buf bytes.Buffer
|
|
godoc.ToHTML(&buf, v, nil)
|
|
p := buf.Bytes()
|
|
p = replaceAll(p, h3Pat, func(out, src []byte, m []int) []byte {
|
|
out = append(out, `<h4 id="`...)
|
|
out = append(out, src[m[2]:m[3]]...)
|
|
out = append(out, `">`...)
|
|
out = append(out, src[m[4]:m[5]]...)
|
|
out = append(out, ` <a class="permalink" href="#`...)
|
|
out = append(out, src[m[2]:m[3]]...)
|
|
out = append(out, `">¶</a></h4>`...)
|
|
return out
|
|
})
|
|
p = replaceAll(p, rfcPat, func(out, src []byte, m []int) []byte {
|
|
out = append(out, `<a href="http://tools.ietf.org/html/rfc`...)
|
|
out = append(out, src[m[2]:m[3]]...)
|
|
|
|
// If available, add section fragment
|
|
if m[4] != -1 {
|
|
out = append(out, `#section-`...)
|
|
out = append(out, src[m[6]:m[7]]...)
|
|
}
|
|
|
|
out = append(out, `">`...)
|
|
out = append(out, src[m[0]:m[1]]...)
|
|
out = append(out, `</a>`...)
|
|
return out
|
|
})
|
|
p = replaceAll(p, packagePat, func(out, src []byte, m []int) []byte {
|
|
path := bytes.TrimRight(src[m[2]:m[3]], ".!?:")
|
|
if !gosrc.IsValidPath(string(path)) {
|
|
return append(out, src[m[0]:m[1]]...)
|
|
}
|
|
out = append(out, src[m[0]:m[2]]...)
|
|
out = append(out, `<a href="/`...)
|
|
out = append(out, path...)
|
|
out = append(out, `">`...)
|
|
out = append(out, path...)
|
|
out = append(out, `</a>`...)
|
|
out = append(out, src[m[2]+len(path):m[1]]...)
|
|
return out
|
|
})
|
|
return htemp.HTML(p)
|
|
}
|
|
|
|
// commentTextFn formats a source code comment as text.
|
|
func commentTextFn(v string) string {
|
|
const indent = " "
|
|
var buf bytes.Buffer
|
|
godoc.ToText(&buf, v, indent, "\t", 80-2*len(indent))
|
|
p := buf.Bytes()
|
|
return string(p)
|
|
}
|
|
|
|
var period = []byte{'.'}
|
|
|
|
func codeFn(c doc.Code, typ *doc.Type) htemp.HTML {
|
|
var buf bytes.Buffer
|
|
last := 0
|
|
src := []byte(c.Text)
|
|
buf.WriteString("<pre>")
|
|
for _, a := range c.Annotations {
|
|
htemp.HTMLEscape(&buf, src[last:a.Pos])
|
|
switch a.Kind {
|
|
case doc.PackageLinkAnnotation:
|
|
buf.WriteString(`<a href="`)
|
|
buf.WriteString(formatPathFrag(c.Paths[a.PathIndex], ""))
|
|
buf.WriteString(`">`)
|
|
htemp.HTMLEscape(&buf, src[a.Pos:a.End])
|
|
buf.WriteString(`</a>`)
|
|
case doc.LinkAnnotation, doc.BuiltinAnnotation:
|
|
var p string
|
|
if a.Kind == doc.BuiltinAnnotation {
|
|
p = "builtin"
|
|
} else if a.PathIndex >= 0 {
|
|
p = c.Paths[a.PathIndex]
|
|
}
|
|
n := src[a.Pos:a.End]
|
|
n = n[bytes.LastIndex(n, period)+1:]
|
|
buf.WriteString(`<a href="`)
|
|
buf.WriteString(formatPathFrag(p, string(n)))
|
|
buf.WriteString(`">`)
|
|
htemp.HTMLEscape(&buf, src[a.Pos:a.End])
|
|
buf.WriteString(`</a>`)
|
|
case doc.CommentAnnotation:
|
|
buf.WriteString(`<span class="com">`)
|
|
htemp.HTMLEscape(&buf, src[a.Pos:a.End])
|
|
buf.WriteString(`</span>`)
|
|
case doc.AnchorAnnotation:
|
|
buf.WriteString(`<span id="`)
|
|
if typ != nil {
|
|
htemp.HTMLEscape(&buf, []byte(typ.Name))
|
|
buf.WriteByte('.')
|
|
}
|
|
htemp.HTMLEscape(&buf, src[a.Pos:a.End])
|
|
buf.WriteString(`">`)
|
|
htemp.HTMLEscape(&buf, src[a.Pos:a.End])
|
|
buf.WriteString(`</span>`)
|
|
default:
|
|
htemp.HTMLEscape(&buf, src[a.Pos:a.End])
|
|
}
|
|
last = int(a.End)
|
|
}
|
|
htemp.HTMLEscape(&buf, src[last:])
|
|
buf.WriteString("</pre>")
|
|
return htemp.HTML(buf.String())
|
|
}
|
|
|
|
var isInterfacePat = regexp.MustCompile(`^type [^ ]+ interface`)
|
|
|
|
func isInterfaceFn(t *doc.Type) bool {
|
|
return isInterfacePat.MatchString(t.Decl.Text)
|
|
}
|
|
|
|
var gaAccount string
|
|
|
|
func gaAccountFn() string {
|
|
return gaAccount
|
|
}
|
|
|
|
func noteTitleFn(s string) string {
|
|
return strings.Title(strings.ToLower(s))
|
|
}
|
|
|
|
func htmlCommentFn(s string) htemp.HTML {
|
|
return htemp.HTML("<!-- " + s + " -->")
|
|
}
|
|
|
|
var mimeTypes = map[string]string{
|
|
".html": htmlMIMEType,
|
|
".txt": textMIMEType,
|
|
}
|
|
|
|
func executeTemplate(resp http.ResponseWriter, name string, status int, header http.Header, data interface{}) error {
|
|
for k, v := range header {
|
|
resp.Header()[k] = v
|
|
}
|
|
mimeType, ok := mimeTypes[path.Ext(name)]
|
|
if !ok {
|
|
mimeType = textMIMEType
|
|
}
|
|
resp.Header().Set("Content-Type", mimeType)
|
|
t := templates[name]
|
|
if t == nil {
|
|
return fmt.Errorf("template %s not found", name)
|
|
}
|
|
resp.WriteHeader(status)
|
|
if status == http.StatusNotModified {
|
|
return nil
|
|
}
|
|
return t.Execute(resp, data)
|
|
}
|
|
|
|
var templates = map[string]interface {
|
|
Execute(io.Writer, interface{}) error
|
|
}{}
|
|
|
|
func joinTemplateDir(base string, files []string) []string {
|
|
result := make([]string, len(files))
|
|
for i := range files {
|
|
result[i] = filepath.Join(base, "templates", files[i])
|
|
}
|
|
return result
|
|
}
|
|
|
|
func parseHTMLTemplates(sets [][]string) error {
|
|
for _, set := range sets {
|
|
templateName := set[0]
|
|
t := htemp.New("")
|
|
t.Funcs(htemp.FuncMap{
|
|
"code": codeFn,
|
|
"comment": commentFn,
|
|
"equal": reflect.DeepEqual,
|
|
"gaAccount": gaAccountFn,
|
|
"host": hostFn,
|
|
"htmlComment": htmlCommentFn,
|
|
"importPath": importPathFn,
|
|
"isInterface": isInterfaceFn,
|
|
"isValidImportPath": gosrc.IsValidPath,
|
|
"map": mapFn,
|
|
"noteTitle": noteTitleFn,
|
|
"relativePath": relativePathFn,
|
|
"sidebarEnabled": func() bool { return viper.GetBool(ConfigSidebar) },
|
|
"staticPath": func(p string) string { return cacheBusters.AppendQueryParam(p, "v") },
|
|
"templateName": func() string { return templateName },
|
|
})
|
|
if _, err := t.ParseFiles(joinTemplateDir(viper.GetString(ConfigAssetsDir), set)...); err != nil {
|
|
return err
|
|
}
|
|
t = t.Lookup("ROOT")
|
|
if t == nil {
|
|
return fmt.Errorf("ROOT template not found in %v", set)
|
|
}
|
|
templates[set[0]] = t
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func parseTextTemplates(sets [][]string) error {
|
|
for _, set := range sets {
|
|
t := ttemp.New("")
|
|
t.Funcs(ttemp.FuncMap{
|
|
"comment": commentTextFn,
|
|
})
|
|
if _, err := t.ParseFiles(joinTemplateDir(viper.GetString(ConfigAssetsDir), set)...); err != nil {
|
|
return err
|
|
}
|
|
t = t.Lookup("ROOT")
|
|
if t == nil {
|
|
return fmt.Errorf("ROOT template not found in %v", set)
|
|
}
|
|
templates[set[0]] = t
|
|
}
|
|
return nil
|
|
}
|