- Upgrade to BS3 final.
- Improve crawling.
- Protect GitHub API quota from unfriendly robots.
- Other small fixes.
This commit is contained in:
Gary Burd 2013-08-26 17:11:09 -07:00
Родитель 834a0afe85
Коммит bc5d91c919
24 изменённых файлов: 4214 добавлений и 1931 удалений

Просмотреть файл

@ -170,12 +170,6 @@ var putScript = redis.NewScript(0, `
redis.call('SREM', 'index:' .. term, id)
elseif x == 2 then
redis.call('SADD', 'index:' .. term, id)
if string.sub(term, 1, 7) == 'import:' then
local import = string.sub(term, 8)
if redis.call('HEXISTS', 'ids', import) == 0 and redis.call('SISMEMBER', 'badCrawl', import) == 0 then
redis.call('SADD', 'newCrawl', import)
end
end
end
end
@ -190,6 +184,15 @@ var putScript = redis.NewScript(0, `
return redis.call('HMSET', 'pkg:' .. id, 'path', path, 'synopsis', synopsis, 'score', score, 'gob', gob, 'terms', terms, 'etag', etag, 'kind', kind)
`)
var addCrawlScript = redis.NewScript(0, `
for i=1,#ARGV do
local pkg = ARGV[i]
if redis.call('HEXISTS', 'ids', pkg) == 0 and redis.call('SISMEMBER', 'badCrawl', pkg) == 0 then
redis.call('SADD', 'newCrawl', pkg)
end
end
`)
// Put adds the package documentation to the database.
func (db *Database) Put(pdoc *doc.Package, nextCrawl time.Time) error {
c := db.Pool.Get()
@ -204,7 +207,7 @@ func (db *Database) Put(pdoc *doc.Package, nextCrawl time.Time) error {
}
// Truncate large documents.
if gobBuf.Len() > 700000 {
if gobBuf.Len() > 200000 {
pdocNew := *pdoc
pdoc = &pdocNew
pdoc.Truncated = true
@ -236,7 +239,45 @@ func (db *Database) Put(pdoc *doc.Package, nextCrawl time.Time) error {
if !nextCrawl.IsZero() {
t = nextCrawl.Unix()
}
_, err = putScript.Do(c, pdoc.ImportPath, pdoc.Synopsis, score, gobBytes, strings.Join(terms, " "), pdoc.Etag, kind, t)
if err != nil {
return err
}
if nextCrawl.IsZero() {
// Skip crawling related packages if this is not a full save.
return nil
}
paths := make(map[string]bool)
for _, p := range pdoc.Imports {
if doc.IsValidRemotePath(p) {
paths[p] = true
}
}
for _, p := range pdoc.TestImports {
if doc.IsValidRemotePath(p) {
paths[p] = true
}
}
for _, p := range pdoc.XTestImports {
if doc.IsValidRemotePath(p) {
paths[p] = true
}
}
if pdoc.ImportPath != pdoc.ProjectRoot && pdoc.ProjectRoot != "" {
paths[pdoc.ProjectRoot] = true
}
for _, p := range pdoc.Subdirectories {
paths[pdoc.ImportPath+"/"+p] = true
}
args := make([]interface{}, 0, len(paths))
for p := range paths {
args = append(args, p)
}
_, err = addCrawlScript.Do(c, args...)
return err
}
@ -456,7 +497,6 @@ var deleteScript = redis.NewScript(0, `
end
redis.call('ZREM', 'nextCrawl', id)
redis.call('SREM', 'badCrawl', path)
redis.call('SREM', 'newCrawl', path)
redis.call('ZREM', 'popular', id)
redis.call('DEL', 'pkg:' .. id)
@ -670,6 +710,7 @@ type PackageInfo struct {
Pkgs []Package
Score float64
Kind string
Size int
}
// Do executes function f for each document in the database.
@ -681,18 +722,20 @@ func (db *Database) Do(f func(*PackageInfo) error) error {
return err
}
for _, key := range keys {
values, err := redis.Values(c.Do("HMGET", key, "gob", "score", "kind", "path"))
values, err := redis.Values(c.Do("HMGET", key, "gob", "score", "kind", "path", "terms", "synopis"))
if err != nil {
return err
}
var (
pi PackageInfo
p []byte
path string
pi PackageInfo
p []byte
path string
terms string
synopsis string
)
if _, err := redis.Scan(values, &p, &pi.Score, &pi.Kind, &path); err != nil {
if _, err := redis.Scan(values, &p, &pi.Score, &pi.Kind, &path, &terms, &synopsis); err != nil {
return err
}
@ -700,6 +743,8 @@ func (db *Database) Do(f func(*PackageInfo) error) error {
continue
}
pi.Size = len(path) + len(p) + len(terms) + len(synopsis)
p, err = snappy.Decode(nil, p)
if err != nil {
return fmt.Errorf("snappy decoding %s: %v", path, err)
@ -810,7 +855,7 @@ func (db *Database) GetGob(key string, value interface{}) error {
return gob.NewDecoder(bytes.NewReader(p)).Decode(value)
}
var incrementPopularScore = redis.NewScript(0, `
var incrementPopularScoreScript = redis.NewScript(0, `
local path = ARGV[1]
local n = ARGV[2]
local t = ARGV[3]
@ -832,20 +877,21 @@ var incrementPopularScore = redis.NewScript(0, `
const popularHalfLife = time.Hour * 24 * 7
func scaledTime(t time.Time) float64 {
const lambda = math.Ln2 / float64(popularHalfLife)
return lambda * float64(t.Sub(time.Unix(1257894000, 0)))
}
func (db *Database) IncrementPopularScore(path string) error {
func (db *Database) incrementPopularScoreInternal(path string, delta float64, t time.Time) error {
// nt = n0 * math.Exp(-lambda * t)
// lambda = math.Ln2 / thalf
c := db.Pool.Get()
defer c.Close()
_, err := incrementPopularScore.Do(c, path, 1, scaledTime(time.Now()))
const lambda = math.Ln2 / float64(popularHalfLife)
scaledTime := lambda * float64(t.Sub(time.Unix(1257894000, 0)))
_, err := incrementPopularScoreScript.Do(c, path, delta, scaledTime)
return err
}
func (db *Database) IncrementPopularScore(path string) error {
return db.incrementPopularScoreInternal(path, 1, time.Now())
}
var popularScript = redis.NewScript(0, `
local stop = ARGV[1]
local ids = redis.call('ZREVRANGE', 'popular', '0', stop)
@ -892,26 +938,59 @@ func (db *Database) PopularWithScores() ([]Package, error) {
return pkgs, err
}
func (db *Database) GetNewCrawl() (string, error) {
func (db *Database) PopNewCrawl() (string, bool, error) {
c := db.Pool.Get()
defer c.Close()
v, err := redis.String(c.Do("SRANDMEMBER", "newCrawl"))
if err == redis.ErrNil {
var subdirs []Package
path, err := redis.String(c.Do("SPOP", "newCrawl"))
switch {
case err == redis.ErrNil:
err = nil
path = ""
case err == nil:
subdirs, err = db.getSubdirs(c, path, nil)
}
return v, err
return path, len(subdirs) > 0, err
}
var setBadCrawlScript = redis.NewScript(0, `
local path = ARGV[1]
if redis.call('SREM', 'newCrawl', path) == 1 then
redis.call('SADD', 'badCrawl', path)
end
`)
func (db *Database) SetBadCrawl(path string) error {
func (db *Database) AddBadCrawl(path string) error {
c := db.Pool.Get()
defer c.Close()
_, err := setBadCrawlScript.Do(c, path)
_, err := c.Do("SADD", "badCrawl", path)
return err
}
var incrementCounterScript = redis.NewScript(0, `
local key = 'counter:' .. ARGV[1]
local n = tonumber(ARGV[2])
local t = tonumber(ARGV[3])
local exp = tonumber(ARGV[4])
local counter = redis.call('GET', key)
if counter then
counter = cjson.decode(counter)
n = n + counter.n * math.exp(counter.t - t)
end
redis.call('SET', key, cjson.encode({n = n; t = t}))
redis.call('EXPIRE', key, exp)
return tostring(n)
`)
const counterHalflife = time.Hour
func (db *Database) incrementCounterInternal(key string, delta float64, t time.Time) (float64, error) {
// nt = n0 * math.Exp(-lambda * t)
// lambda = math.Ln2 / thalf
c := db.Pool.Get()
defer c.Close()
const lambda = math.Ln2 / float64(counterHalflife)
scaledTime := lambda * float64(t.Sub(time.Unix(1257894000, 0)))
return redis.Float64(incrementCounterScript.Do(c, key, delta, scaledTime, (4*counterHalflife)/time.Second))
}
func (db *Database) IncrementCounter(key string, delta float64) (float64, error) {
return db.incrementCounterInternal(key, delta, time.Now())
}

Просмотреть файл

@ -193,6 +193,8 @@ func TestPutGet(t *testing.T) {
}
}
const epsilon = 0.000001
func TestPopular(t *testing.T) {
db := newDB(t)
defer closeDB(db)
@ -207,7 +209,7 @@ func TestPopular(t *testing.T) {
for id := 12; id >= 0; id-- {
path := "github.com/user/repo/p" + strconv.Itoa(id)
c.Do("HSET", "ids", path, id)
_, err := incrementPopularScore.Do(c, path, score, scaledTime(now))
err := db.incrementPopularScoreInternal(path, score, now)
if err != nil {
t.Fatal(err)
}
@ -227,8 +229,39 @@ func TestPopular(t *testing.T) {
}
for i := 3; i < len(values); i += 2 {
s, _ := redis.Float64(values[i], nil)
if math.Abs(score-s)/score > 0.0001 {
if math.Abs(score-s)/score > epsilon {
t.Errorf("Bad score, score[1]=%g, score[%d]=%g", score, i, s)
}
}
}
func TestCounter(t *testing.T) {
db := newDB(t)
defer closeDB(db)
const key = "127.0.0.1"
now := time.Now()
n, err := db.incrementCounterInternal(key, 1, now)
if err != nil {
t.Fatal(err)
}
if math.Abs(n-1.0) > epsilon {
t.Errorf("1: got n=%g, want 1", n)
}
n, err = db.incrementCounterInternal(key, 1, now)
if err != nil {
t.Fatal(err)
}
if math.Abs(n-2.0)/2.0 > epsilon {
t.Errorf("2: got n=%g, want 2", n)
}
now = now.Add(counterHalflife)
n, err = db.incrementCounterInternal(key, 1, now)
if err != nil {
t.Fatal(err)
}
if math.Abs(n-2.0)/2.0 > epsilon {
t.Errorf("3: got n=%g, want 2", n)
}
}

Просмотреть файл

@ -63,18 +63,19 @@ func getBitbucketDoc(client *http.Client, match map[string]string, savedEtag str
return nil, ErrNotModified
}
var directory struct {
Files []struct {
var contents struct {
Directories []string
Files []struct {
Path string
}
}
if err := httpGetJSON(client, expand("https://api.bitbucket.org/1.0/repositories/{owner}/{repo}/src/{tag}{dir}/", match), nil, &directory); err != nil {
if err := httpGetJSON(client, expand("https://api.bitbucket.org/1.0/repositories/{owner}/{repo}/src/{tag}{dir}/", match), nil, &contents); err != nil {
return nil, err
}
var files []*source
for _, f := range directory.Files {
for _, f := range contents.Files {
_, name := path.Split(f.Path)
if isDocFile(name) {
files = append(files, &source{
@ -91,14 +92,15 @@ func getBitbucketDoc(client *http.Client, match map[string]string, savedEtag str
b := builder{
pdoc: &Package{
LineFmt: "%s#cl-%d",
ImportPath: match["originalImportPath"],
ProjectRoot: expand("bitbucket.org/{owner}/{repo}", match),
ProjectName: match["repo"],
ProjectURL: expand("https://bitbucket.org/{owner}/{repo}/", match),
BrowseURL: expand("https://bitbucket.org/{owner}/{repo}/src/{tag}{dir}", match),
Etag: etag,
VCS: match["vcs"],
LineFmt: "%s#cl-%d",
ImportPath: match["originalImportPath"],
ProjectRoot: expand("bitbucket.org/{owner}/{repo}", match),
ProjectName: match["repo"],
ProjectURL: expand("https://bitbucket.org/{owner}/{repo}/", match),
BrowseURL: expand("https://bitbucket.org/{owner}/{repo}/src/{tag}{dir}", match),
Etag: etag,
VCS: match["vcs"],
Subdirectories: contents.Directories,
},
}

Просмотреть файл

@ -16,6 +16,7 @@ package doc
import (
"bytes"
"errors"
"go/ast"
"go/build"
"go/doc"
@ -327,19 +328,22 @@ var packageNamePats = []*regexp.Regexp{
func simpleImporter(imports map[string]*ast.Object, path string) (*ast.Object, error) {
pkg := imports[path]
if pkg == nil {
// Guess the package name without importing it.
for _, pat := range packageNamePats {
m := pat.FindStringSubmatch(path)
if m != nil {
pkg = ast.NewObj(ast.Pkg, m[1])
pkg.Data = ast.NewScope(nil)
imports[path] = pkg
break
}
if pkg != nil {
return pkg, nil
}
// Guess the package name without importing it.
for _, pat := range packageNamePats {
m := pat.FindStringSubmatch(path)
if m != nil {
pkg = ast.NewObj(ast.Pkg, m[1])
pkg.Data = ast.NewScope(nil)
imports[path] = pkg
return pkg, nil
}
}
return pkg, nil
return nil, errors.New("package not found")
}
type File struct {

Просмотреть файл

@ -17,7 +17,6 @@ package doc
import (
"net/http"
"net/url"
"path"
"regexp"
"strings"
"time"
@ -73,49 +72,91 @@ func getGitHubDoc(client *http.Client, match map[string]string, savedEtag string
return nil, ErrNotModified
}
var tree struct {
Tree []struct {
Url string
Path string
Type string
}
Url string
var contents []*struct {
Type string
Name string
Git_URL string
HTML_URL string
}
err = httpGetJSON(client, expand("https://api.github.com/repos/{owner}/{repo}/git/trees/{tag}?recursive=1&{cred}", match), nil, &tree)
err = httpGetJSON(client, expand("https://api.github.com/repos/{owner}/{repo}/contents{dir}?ref={tag}&{cred}", match), nil, &contents)
if err != nil {
return nil, err
}
// Because Github API URLs are case-insensitive, we need to check that the
// userRepo returned from Github matches the one that we are requesting.
if !strings.HasPrefix(tree.Url, expand("https://api.github.com/repos/{owner}/{repo}/", match)) {
if len(contents) == 0 {
return nil, NotFoundError{"No files in directory."}
}
// Because Github API URLs are case-insensitive, we check that the owner
// and repo returned from Github matches the one that we are requesting.
if !strings.HasPrefix(contents[0].Git_URL, expand("https://api.github.com/repos/{owner}/{repo}/", match)) {
return nil, NotFoundError{"Github import path has incorrect case."}
}
inTree := false
dirPrefix := match["dir"]
if dirPrefix != "" {
dirPrefix = dirPrefix[1:] + "/"
}
var files []*source
for _, node := range tree.Tree {
if node.Type != "blob" || !strings.HasPrefix(node.Path, dirPrefix) {
continue
}
inTree = true
if d, f := path.Split(node.Path); d == dirPrefix && isDocFile(f) {
var subdirs []string
for _, item := range contents {
switch {
case item.Type == "dir":
if isValidPathElement(item.Name) {
subdirs = append(subdirs, item.Name)
}
case isDocFile(item.Name):
files = append(files, &source{
name: f,
browseURL: expand("https://github.com/{owner}/{repo}/blob/{tag}/{0}", match, node.Path),
rawURL: node.Url + "?" + gitHubCred,
name: item.Name,
browseURL: item.HTML_URL,
rawURL: item.Git_URL + "?" + gitHubCred,
})
}
}
if !inTree {
return nil, NotFoundError{"Directory tree does not contain Go files."}
}
/*
var tree struct {
Tree []struct {
Url string
Path string
Type string
}
Url string
}
err = httpGetJSON(client, expand("https://api.github.com/repos/{owner}/{repo}/git/trees/{tag}?recursive=1&{cred}", match), nil, &tree)
if err != nil {
return nil, err
}
// Because Github API URLs are case-insensitive, we need to check that the
// userRepo returned from Github matches the one that we are requesting.
if !strings.HasPrefix(tree.Url, expand("https://api.github.com/repos/{owner}/{repo}/", match)) {
return nil, NotFoundError{"Github import path has incorrect case."}
}
inTree := false
dirPrefix := match["dir"]
if dirPrefix != "" {
dirPrefix = dirPrefix[1:] + "/"
}
var files []*source
for _, node := range tree.Tree {
if node.Type != "blob" || !strings.HasPrefix(node.Path, dirPrefix) {
continue
}
inTree = true
if d, f := path.Split(node.Path); d == dirPrefix && isDocFile(f) {
files = append(files, &source{
name: f,
browseURL: expand("https://github.com/{owner}/{repo}/blob/{tag}/{0}", match, node.Path),
rawURL: node.Url + "?" + gitHubCred,
})
}
}
if !inTree {
return nil, NotFoundError{"Directory tree does not contain Go files."}
}
*/
if err := fetchFiles(client, files, gitHubRawHeader); err != nil {
return nil, err
@ -128,14 +169,15 @@ func getGitHubDoc(client *http.Client, match map[string]string, savedEtag string
b := &builder{
pdoc: &Package{
LineFmt: "%s#L%d",
ImportPath: match["originalImportPath"],
ProjectRoot: expand("github.com/{owner}/{repo}", match),
ProjectName: match["repo"],
ProjectURL: expand("https://github.com/{owner}/{repo}", match),
BrowseURL: browseURL,
Etag: commit,
VCS: "git",
LineFmt: "%s#L%d",
ImportPath: match["originalImportPath"],
ProjectRoot: expand("github.com/{owner}/{repo}", match),
ProjectName: match["repo"],
ProjectURL: expand("https://github.com/{owner}/{repo}", match),
BrowseURL: browseURL,
Etag: commit,
VCS: "git",
Subdirectories: subdirs,
},
}

Просмотреть файл

@ -26,7 +26,7 @@ var (
googleRepoRe = regexp.MustCompile(`id="checkoutcmd">(hg|git|svn)`)
googleRevisionRe = regexp.MustCompile(`<h2>(?:[^ ]+ - )?Revision *([^:]+):`)
googleEtagRe = regexp.MustCompile(`^(hg|git|svn)-`)
googleFileRe = regexp.MustCompile(`<li><a href="([^"/]+)"`)
googleFileRe = regexp.MustCompile(`<li><a href="([^"]+)"`)
googlePattern = regexp.MustCompile(`^code\.google\.com/p/(?P<repo>[a-z0-9\-]+)(:?\.(?P<subrepo>[a-z0-9\-]+))?(?P<dir>/[a-z0-9A-Z_.\-/]+)?$`)
)
@ -54,10 +54,17 @@ func getGoogleDoc(client *http.Client, match map[string]string, savedEtag string
}
}
var subdirs []string
var files []*source
for _, m := range googleFileRe.FindAllSubmatch(p, -1) {
fname := string(m[1])
if isDocFile(fname) {
switch {
case strings.HasSuffix(fname, "/"):
fname = fname[:len(fname)-1]
if isValidPathElement(fname) {
subdirs = append(subdirs, fname)
}
case isDocFile(fname):
files = append(files, &source{
name: fname,
browseURL: expand("http://code.google.com/p/{repo}/source/browse{dir}/{0}{query}", match, fname),

Просмотреть файл

@ -487,6 +487,10 @@ var validTLD = map[string]bool{
var validHost = regexp.MustCompile(`^[-a-z0-9]+(?:\.[-a-z0-9]+)+$`)
var validPathElement = regexp.MustCompile(`^[-A-Za-z0-9~+][-A-Za-z0-9_.]*$`)
func isValidPathElement(s string) bool {
return validPathElement.MatchString(s) && s != "testdata"
}
// IsValidRemotePath returns true if importPath is structurally valid for "go get".
func IsValidRemotePath(importPath string) bool {
@ -506,7 +510,7 @@ func IsValidRemotePath(importPath string) bool {
}
for _, part := range parts[1:] {
if !validPathElement.MatchString(part) || part == "testdata" {
if !isValidPathElement(part) {
return false
}
}

Просмотреть файл

@ -89,7 +89,7 @@ type vcsCmd struct {
}
var vcsCmds = map[string]*vcsCmd{
"git": &vcsCmd{
"git": {
schemes: []string{"http", "https", "git"},
download: downloadGit,
},
@ -216,33 +216,39 @@ func getVCSDoc(client *http.Client, match map[string]string, etagSaved string) (
}
var files []*source
var subdirs []string
for _, fi := range fis {
if fi.IsDir() || !isDocFile(fi.Name()) {
continue
switch {
case fi.IsDir():
if isValidPathElement(fi.Name()) {
subdirs = append(subdirs, fi.Name())
}
case isDocFile(fi.Name()):
b, err := ioutil.ReadFile(path.Join(d, fi.Name()))
if err != nil {
return nil, err
}
files = append(files, &source{
name: fi.Name(),
browseURL: expand(template.fileBrowse, urlMatch, fi.Name()),
data: b,
})
}
b, err := ioutil.ReadFile(path.Join(d, fi.Name()))
if err != nil {
return nil, err
}
files = append(files, &source{
name: fi.Name(),
browseURL: expand(template.fileBrowse, urlMatch, fi.Name()),
data: b,
})
}
// Create the documentation.
b := &builder{
pdoc: &Package{
LineFmt: template.line,
ImportPath: match["importPath"],
ProjectRoot: expand("{repo}.{vcs}", match),
ProjectName: path.Base(match["repo"]),
ProjectURL: expand(template.project, urlMatch),
BrowseURL: "",
Etag: etag,
VCS: match["vcs"],
LineFmt: template.line,
ImportPath: match["importPath"],
ProjectRoot: expand("{repo}.{vcs}", match),
ProjectName: path.Base(match["repo"]),
ProjectURL: expand(template.project, urlMatch),
BrowseURL: "",
Etag: etag,
VCS: match["vcs"],
Subdirectories: subdirs,
},
}

Просмотреть файл

@ -15,8 +15,10 @@
package main
import (
"fmt"
"log"
"os"
"sort"
"github.com/garyburd/gddo/database"
)
@ -27,14 +29,47 @@ var statsCommand = &command{
usage: "stats",
}
type itemSize struct {
path string
size int
}
type bySizeDesc []itemSize
func (p bySizeDesc) Len() int { return len(p) }
func (p bySizeDesc) Less(i, j int) bool { return p[i].size > p[j].size }
func (p bySizeDesc) Swap(i, j int) { p[i], p[j] = p[j], p[i] }
func stats(c *command) {
if len(c.flag.Args()) != 0 {
c.printUsage()
os.Exit(1)
}
_, err := database.New()
db, err := database.New()
if err != nil {
log.Fatal(err)
}
log.Println("DONE")
var packageSizes []itemSize
projectSizes := make(map[string]int)
err = db.Do(func(pi *database.PackageInfo) error {
packageSizes = append(packageSizes, itemSize{pi.PDoc.ImportPath, pi.Size})
projectSizes[pi.PDoc.ProjectRoot] += pi.Size
return nil
})
var sizes []itemSize
for path, size := range projectSizes {
sizes = append(sizes, itemSize{path, size})
}
sort.Sort(bySizeDesc(sizes))
for _, size := range sizes {
fmt.Printf("%6d %s\n", size.size, size.path)
}
sort.Sort(bySizeDesc(packageSizes))
for _, size := range packageSizes {
fmt.Printf("%6d %s\n", size.size, size.path)
}
}

Двоичные данные
gddo-server/assets/favicon.ico Normal file → Executable file

Двоичный файл не отображается.

До

Ширина:  |  Высота:  |  Размер: 198 B

После

Ширина:  |  Высота:  |  Размер: 1.4 KiB

Просмотреть файл

@ -2,7 +2,7 @@ html { background-color: whitesmoke; }
body { background-color: white; }
h4 { margin-top: 20px; }
.container { max-width: 728px; }
#x-projnav {
min-height: 20px;
margin-bottom: 20px;
@ -12,10 +12,13 @@ h4 { margin-top: 20px; }
}
#x-footer {
padding-top: 15px;
padding-top: 14px;
padding-bottom: 15px;
margin-top: 5px;
background-color: #eee;
border-top-style: solid;
border-top-width: 1px;
}
#x-pkginfo {
@ -25,12 +28,6 @@ h4 { margin-top: 20px; }
margin-bottom: 15px;
}
@media screen and (min-width: 768px) {
#x-search {
float: right;
}
}
code {
background-color: inherit;
border: none;
@ -55,22 +52,29 @@ pre .com {
color: rgb(147, 161, 161);
}
a, .navbar-brand {
a, .navbar-default .navbar-brand {
color: #375eab;
}
.btn-default {
background-color: #375eab;
.navbar-default, #x-footer {
background-color: hsl(209, 51%, 92%);
border-color: hsl(209, 51%, 88%);
}
.navbar, #x-footer {
background-color: #E0EBF5;
.navbar-default .navbar-nav > .active > a,
.navbar-default .navbar-nav > .active > a:hover,
.navbar-default .navbar-nav > .active > a:focus {
background-color: hsl(209, 51%, 88%);
}
.navbar-nav > .active > a,
.navbar-nav > .active > a:hover,
.navbar-nav > .active > a:focus {
background-color: #e7e7e7;
.navbar-default .navbar-nav > li > a:hover,
.navbar-default .navbar-nav > li > a:focus {
color: #000;
}
.panel-default > .panel-heading {
color: #333;
background-color: transparent;
}
#x-file .highlight {

Просмотреть файл

@ -72,8 +72,10 @@ $(function() {
$('span.timeago').timeago();
if (window.location.hash.substring(0, 9) == '#example-') {
console.log(window.location.hash.substring(1, 9));
$('#ex-' + window.location.hash.substring(9)).addClass('in').height('auto');
var id = '#ex-' + window.location.hash.substring(9);
console.log(id);
console.log($(id));
$(id).addClass('in').removeClass('collapse').height('auto');
}
var highlighted;

Просмотреть файл

@ -15,7 +15,7 @@ and more.
<div class="row">
<div class="col-sm-6">
{{with .Popular}}
<h4>Popular Packages</h4>
<h4>Popular Projects</h4>
<ul class="list-unstyled">
{{range .}}<li><a href="/{{.Path}}">{{.Path}}</a>{{end}}
</ul>
@ -28,6 +28,7 @@ and more.
<li><a href="/-/go">Go Standard Packages</a>
<li><a href="/-/subrepo">Go Sub-repository Packages</a>
<li><a href="https://code.google.com/p/go-wiki/wiki/Projects">Projects @ go-wiki</a>
<li><a href="http://go-search.org/">Go Search</a>
</ul>
</div>
</div>

Просмотреть файл

@ -6,34 +6,38 @@
{{template "Head" $}}
</head>
<body>
<div class="navbar navbar-static-top">
<nav class="navbar navbar-default" role="navigation">
<div class="container">
<button type="button" class="navbar-toggle" data-toggle="collapse" data-target=".navbar-responsive-collapse">
<div class="navbar-header">
<button type="button" class="navbar-toggle" data-toggle="collapse" data-target=".navbar-collapse">
<span class="sr-only">Toggle navigation</span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
</button>
<a class="navbar-brand" href="/">GoDoc</a>
<div class="nav-collapse collapse navbar-responsive-collapse">
<ul class="nav navbar-nav">
<a class="navbar-brand" href="/"><strong>GoDoc</strong></a>
</div>
<div class="collapse navbar-collapse">
<ul class="nav navbar-nav">
<li{{if equal "home.html" templateName}} class="active"{{end}}><a href="/">Home</a></li>
<li{{if equal "index.html" templateName}} class="active"{{end}}><a href="/-/index">Index</a></li>
<li{{if equal "about.html" templateName}} class="active"{{end}}><a href="/-/about">About</a></li>
</ul>
<form class="navbar-form" id="x-search" action="/"><input class="form-control" id="x-search-query" type="text" name="q" placeholder="Search"></form>
</div>
</ul>
<form class="navbar-nav navbar-form navbar-right" id="x-search" action="/" role="search"><input class="form-control" id="x-search-query" type="text" name="q" placeholder="Search"></form>
</div>
</div>
<div class="container"><div class="row"><div class="col-12">
</nav>
<div class="container">
{{template "Body" $}}
</div></div></div>
</div>
<div id="x-footer" class="clearfix">
<div class="container"><div class="row"><div class="col-12">
<div class="container">
<a href="mailto:info@godoc.org">Feedback</a>
<span class="text-muted">|</span> <a href="https://github.com/garyburd/gddo/issues">Website Issues</a>
<span class="text-muted">|</span> <a href="http://golang.org/">Go Language</a>
<span class="pull-right"><a href="#">Back to top</a></span>
</div></div></div>
</div>
</div>
<div id="x-shortcuts" tabindex="-1" class="modal fade">

Просмотреть файл

@ -25,7 +25,7 @@
</ul>
{{with .AllExamples}}<h4 id="pkg-examples">Examples <a class="permalink" href="#pkg-examples">&para;</a></h4><ul class="list-unstyled">{{range . }}
<li><a href="#example-{{.Id}}" onclick="$('#ex-{{.Id}}').addClass('in').height('auto')">{{.Label}}</a>{{end}}
<li><a href="#example-{{.Id}}" onclick="$('#ex-{{.Id}}').addClass('in').removeClass('collapse').height('auto')">{{.Label}}</a>{{end}}
</ul>{{else}}<span id="pkg-examples"></span>{{end}}
<h4 id="pkg-files">{{with .BrowseURL}}<a href="{{.}}">Package Files</a>{{else}}Package Files{{end}} <a class="permalink" href="#pkg-files">&para;</a></h4>
@ -65,10 +65,10 @@
{{template "PkgCmdFooter" $}}
{{end}}
{{define "Examples"}}{{if .}}<div class="accordian">{{range .}}
<div class="accordion-group">
<div class="accordion-heading"><a class="accordion-toggle" data-toggle="collapse" id="example-{{.Id}}" href="#ex-{{.Id}}">Example{{with .Example.Name}} ({{.}}){{end}}</a></div>
<div id="ex-{{.Id}}" class="accordion-body collapse"><div class="accordion-inner">
{{define "Examples"}}{{if .}}<div class="panel-group">{{range .}}
<div class="panel panel-default" id="example-{{.Id}}">
<div class="panel-heading"><a class="accordion-toggle" data-toggle="collapse" href="#ex-{{.Id}}">Example{{with .Example.Name}} ({{.}}){{end}}</a></div>
<div id="ex-{{.Id}}" class="panel-collapse collapse"><div class="panel-body">
{{with .Example.Doc}}<p>{{.|comment}}{{end}}
<p>Code:{{if .Example.Play}}<span class="pull-right"><a href="?play={{.Id}}">play</a>&nbsp;</span>{{end}}
<pre>{{code .Example.Code nil}}</pre>

Просмотреть файл

@ -4,6 +4,8 @@
<div class="well">
{{template "SearchBox" .q}}
</div>
<p>Search on <a href="http://go-search.org/search?q={{.q}}">Go-Search</a>
or <a href="https://github.com/search?q={{.q}}+language:go">GitHub</a>.
{{if .pkgs}}
{{template "Pkgs" .pkgs}}
{{else}}

Разница между файлами не показана из-за своего большого размера Загрузить разницу

Различия файлов скрыты, потому что одна или несколько строк слишком длинны

Просмотреть файл

@ -49,7 +49,7 @@ if (!jQuery) { throw new Error("Bootstrap requires jQuery") }
// http://blog.alexmaccaw.com/css-transitions
$.fn.emulateTransitionEnd = function (duration) {
var called = false, $el = this
var called = false, $el = this
$(this).one($.support.transition.end, function () { called = true })
var callback = function () { if (!called) $($el).trigger($.support.transition.end) }
setTimeout(callback, duration)
@ -314,6 +314,7 @@ if (!jQuery) { throw new Error("Bootstrap requires jQuery") }
Carousel.DEFAULTS = {
interval: 5000
, pause: 'hover'
, wrap: true
}
Carousel.prototype.cycle = function (e) {
@ -378,12 +379,15 @@ if (!jQuery) { throw new Error("Bootstrap requires jQuery") }
var fallback = type == 'next' ? 'first' : 'last'
var that = this
if (!$next.length) {
if (!this.options.wrap) return
$next = this.$element.find('.item')[fallback]()
}
this.sliding = true
isCycling && this.pause()
$next = $next.length ? $next : this.$element.find('.item')[fallback]()
var e = $.Event('slide.bs.carousel', { relatedTarget: $next[0], direction: direction })
if ($next.hasClass('active')) return
@ -535,7 +539,7 @@ if (!jQuery) { throw new Error("Bootstrap requires jQuery") }
this.$element.trigger(startEvent)
if (startEvent.isDefaultPrevented()) return
var actives = this.$parent && this.$parent.find('> .accordion-group > .in')
var actives = this.$parent && this.$parent.find('> .panel > .in')
if (actives && actives.length) {
var hasData = actives.data('bs.collapse')
@ -656,7 +660,7 @@ if (!jQuery) { throw new Error("Bootstrap requires jQuery") }
var $parent = parent && $(parent)
if (!data || !data.transitioning) {
if ($parent) $parent.find('[data-toggle=collapse][data-parent=' + parent + ']').not($this).addClass('collapsed')
if ($parent) $parent.find('[data-toggle=collapse][data-parent="' + parent + '"]').not($this).addClass('collapsed')
$this[$target.hasClass('in') ? 'addClass' : 'removeClass']('collapsed')
}
@ -707,7 +711,7 @@ if (!jQuery) { throw new Error("Bootstrap requires jQuery") }
clearMenus()
if (!isActive) {
if ('ontouchstart' in document.documentElement) {
if ('ontouchstart' in document.documentElement && !$parent.closest('.navbar-nav').length) {
// if mobile we we use a backdrop because click events don't delegate
$('<div class="dropdown-backdrop"/>').insertAfter($(this)).on('click', clearMenus)
}
@ -719,9 +723,9 @@ if (!jQuery) { throw new Error("Bootstrap requires jQuery") }
$parent
.toggleClass('open')
.trigger('shown.bs.dropdown')
}
$this.focus()
$this.focus()
}
return false
}
@ -847,11 +851,11 @@ if (!jQuery) { throw new Error("Bootstrap requires jQuery") }
var Modal = function (element, options) {
this.options = options
this.$element = $(element).on('click.dismiss.modal', '[data-dismiss="modal"]', $.proxy(this.hide, this))
this.$element = $(element)
this.$backdrop =
this.isShown = null
if (this.options.remote) this.$element.find('.modal-body').load(this.options.remote)
if (this.options.remote) this.$element.load(this.options.remote)
}
Modal.DEFAULTS = {
@ -860,13 +864,13 @@ if (!jQuery) { throw new Error("Bootstrap requires jQuery") }
, show: true
}
Modal.prototype.toggle = function () {
return this[!this.isShown ? 'show' : 'hide']()
Modal.prototype.toggle = function (_relatedTarget) {
return this[!this.isShown ? 'show' : 'hide'](_relatedTarget)
}
Modal.prototype.show = function () {
Modal.prototype.show = function (_relatedTarget) {
var that = this
var e = $.Event('show.bs.modal')
var e = $.Event('show.bs.modal', { relatedTarget: _relatedTarget })
this.$element.trigger(e)
@ -876,6 +880,8 @@ if (!jQuery) { throw new Error("Bootstrap requires jQuery") }
this.escape()
this.$element.on('click.dismiss.modal', '[data-dismiss="modal"]', $.proxy(this.hide, this))
this.backdrop(function () {
var transition = $.support.transition && that.$element.hasClass('fade')
@ -895,13 +901,15 @@ if (!jQuery) { throw new Error("Bootstrap requires jQuery") }
that.enforceFocus()
var e = $.Event('shown.bs.modal', { relatedTarget: _relatedTarget })
transition ?
that.$element
that.$element.find('.modal-dialog') // wait for modal to slide in
.one($.support.transition.end, function () {
that.$element.focus().trigger('shown.bs.modal')
that.$element.focus().trigger(e)
})
.emulateTransitionEnd(300) :
that.$element.focus().trigger('shown.bs.modal')
that.$element.focus().trigger(e)
})
}
@ -923,6 +931,7 @@ if (!jQuery) { throw new Error("Bootstrap requires jQuery") }
this.$element
.removeClass('in')
.attr('aria-hidden', true)
.off('click.dismiss.modal')
$.support.transition && this.$element.hasClass('fade') ?
this.$element
@ -975,7 +984,7 @@ if (!jQuery) { throw new Error("Bootstrap requires jQuery") }
this.$backdrop = $('<div class="modal-backdrop ' + animate + '" />')
.appendTo(document.body)
this.$element.on('click', $.proxy(function (e) {
this.$element.on('click.dismiss.modal', $.proxy(function (e) {
if (e.target !== e.currentTarget) return
this.options.backdrop == 'static'
? this.$element[0].focus.call(this.$element[0])
@ -1014,15 +1023,15 @@ if (!jQuery) { throw new Error("Bootstrap requires jQuery") }
var old = $.fn.modal
$.fn.modal = function (option) {
$.fn.modal = function (option, _relatedTarget) {
return this.each(function () {
var $this = $(this)
var data = $this.data('bs.modal')
var options = $.extend({}, Modal.DEFAULTS, $this.data(), typeof option == 'object' && option)
if (!data) $this.data('bs.modal', (data = new Modal(this, options)))
if (typeof option == 'string') data[option]()
else if (options.show) data.show()
if (typeof option == 'string') data[option](_relatedTarget)
else if (options.show) data.show(_relatedTarget)
})
}
@ -1045,28 +1054,26 @@ if (!jQuery) { throw new Error("Bootstrap requires jQuery") }
var $this = $(this)
var href = $this.attr('href')
var $target = $($this.attr('data-target') || (href && href.replace(/.*(?=#[^\s]+$)/, ''))) //strip for ie7
var option = $target.data('modal') ? 'toggle' : $.extend({ remote:!/#/.test(href) && href }, $target.data(), $this.data())
var option = $target.data('modal') ? 'toggle' : $.extend({ remote: !/#/.test(href) && href }, $target.data(), $this.data())
e.preventDefault()
$target
.modal(option)
.modal(option, this)
.one('hide', function () {
$this.is(':visible') && $this.focus()
})
})
$(function () {
var $body = $(document.body)
.on('shown.bs.modal', '.modal', function () { $body.addClass('modal-open') })
.on('hidden.bs.modal', '.modal', function () { $body.removeClass('modal-open') })
})
$(document)
.on('show.bs.modal', '.modal', function () { $(document.body).addClass('modal-open') })
.on('hidden.bs.modal', '.modal', function () { $(document.body).removeClass('modal-open') })
}(window.jQuery);
/* ========================================================================
* Bootstrap: tooltip.js v3.0.0
* http://twbs.github.com/bootstrap/javascript.html#affix
* http://twbs.github.com/bootstrap/javascript.html#tooltip
* Inspired by the original jQuery.tipsy by Jason Frame
* ========================================================================
* Copyright 2012 Twitter, Inc.
@ -1130,7 +1137,7 @@ if (!jQuery) { throw new Error("Bootstrap requires jQuery") }
var eventIn = trigger == 'hover' ? 'mouseenter' : 'focus'
var eventOut = trigger == 'hover' ? 'mouseleave' : 'blur'
this.$element.on(eventIn + '.' + this.type, this.options.selector, $.proxy(this.enter, this))
this.$element.on(eventIn + '.' + this.type, this.options.selector, $.proxy(this.enter, this))
this.$element.on(eventOut + '.' + this.type, this.options.selector, $.proxy(this.leave, this))
}
}
@ -1157,37 +1164,43 @@ if (!jQuery) { throw new Error("Bootstrap requires jQuery") }
return options
}
Tooltip.prototype.enter = function (obj) {
var defaults = this.getDefaults()
Tooltip.prototype.getDelegateOptions = function () {
var options = {}
var defaults = this.getDefaults()
this._options && $.each(this._options, function (key, value) {
if (defaults[key] != value) options[key] = value
})
return options
}
Tooltip.prototype.enter = function (obj) {
var self = obj instanceof this.constructor ?
obj : $(obj.currentTarget)[this.type](options).data('bs.' + this.type)
obj : $(obj.currentTarget)[this.type](this.getDelegateOptions()).data('bs.' + this.type)
clearTimeout(self.timeout)
self.hoverState = 'in'
if (!self.options.delay || !self.options.delay.show) return self.show()
self.hoverState = 'in'
self.timeout = setTimeout(function () {
self.timeout = setTimeout(function () {
if (self.hoverState == 'in') self.show()
}, self.options.delay.show)
}
Tooltip.prototype.leave = function (obj) {
var self = obj instanceof this.constructor ?
obj : $(obj.currentTarget)[this.type](this._options).data('bs.' + this.type)
obj : $(obj.currentTarget)[this.type](this.getDelegateOptions()).data('bs.' + this.type)
clearTimeout(self.timeout)
self.hoverState = 'out'
if (!self.options.delay || !self.options.delay.hide) return self.hide()
self.hoverState = 'out'
self.timeout = setTimeout(function () {
self.timeout = setTimeout(function () {
if (self.hoverState == 'out') self.hide()
}, self.options.delay.hide)
}
@ -1245,7 +1258,7 @@ if (!jQuery) { throw new Error("Bootstrap requires jQuery") }
.addClass(placement)
}
var calculatedOffset = this.getCalcuatedOffset(placement, pos, actualWidth, actualHeight)
var calculatedOffset = this.getCalculatedOffset(placement, pos, actualWidth, actualHeight)
this.applyPlacement(calculatedOffset, placement)
this.$element.trigger('shown.bs.' + this.type)
@ -1320,7 +1333,9 @@ if (!jQuery) { throw new Error("Bootstrap requires jQuery") }
var $tip = this.tip()
var e = $.Event('hide.bs.' + this.type)
function complete() { $tip.detach() }
function complete() {
if (that.hoverState != 'in') $tip.detach()
}
this.$element.trigger(e)
@ -1358,7 +1373,7 @@ if (!jQuery) { throw new Error("Bootstrap requires jQuery") }
}, this.$element.offset())
}
Tooltip.prototype.getCalcuatedOffset = function (placement, pos, actualWidth, actualHeight) {
Tooltip.prototype.getCalculatedOffset = function (placement, pos, actualWidth, actualHeight) {
return placement == 'bottom' ? { top: pos.top + pos.height, left: pos.left + pos.width / 2 - actualWidth / 2 } :
placement == 'top' ? { top: pos.top - actualHeight, left: pos.left + pos.width / 2 - actualWidth / 2 } :
placement == 'left' ? { top: pos.top + pos.height / 2 - actualHeight / 2, left: pos.left - actualWidth } :
@ -1405,7 +1420,7 @@ if (!jQuery) { throw new Error("Bootstrap requires jQuery") }
}
Tooltip.prototype.toggle = function (e) {
var self = e ? $(e.currentTarget)[this.type](this._options).data('bs.' + this.type) : this
var self = e ? $(e.currentTarget)[this.type](this.getDelegateOptions()).data('bs.' + this.type) : this
self.tip().hasClass('in') ? self.leave(self) : self.enter(self)
}
@ -1503,7 +1518,9 @@ if (!jQuery) { throw new Error("Bootstrap requires jQuery") }
$tip.removeClass('fade top bottom left right in')
$tip.find('.popover-title:empty').hide()
// IE8 doesn't accept hiding via the `:empty` pseudo selector, we have to do
// this manually by checking the contents.
if (!$tip.find('.popover-title').html()) $tip.find('.popover-title').hide()
}
Popover.prototype.hasContent = function () {
@ -1520,7 +1537,7 @@ if (!jQuery) { throw new Error("Bootstrap requires jQuery") }
o.content)
}
Popover.prototype.arrow =function () {
Popover.prototype.arrow = function () {
return this.$arrow = this.$arrow || this.tip().find('.arrow')
}

Различия файлов скрыты, потому что одна или несколько строк слишком длинны

Просмотреть файл

@ -65,15 +65,15 @@ func runBackgroundTasks() {
func doCrawl() error {
// Look for new package to crawl.
importPath, err := db.GetNewCrawl()
importPath, hasSubdirs, err := db.PopNewCrawl()
if err != nil {
log.Printf("db.GetNewCrawl() returned error %v", err)
log.Printf("db.PopNewCrawl() returned error %v", err)
return nil
}
if importPath != "" {
if pdoc, err := crawlDoc("new", importPath, nil, false, time.Time{}); err != nil || pdoc == nil {
if err := db.SetBadCrawl(importPath); err != nil {
log.Printf("ERROR db.SetBadCrawl(%q): %v", importPath, err)
if pdoc, err := crawlDoc("new", importPath, nil, hasSubdirs, time.Time{}); pdoc == nil && err == nil {
if err := db.AddBadCrawl(importPath); err != nil {
log.Printf("ERROR db.AddBadCrawl(%q): %v", importPath, err)
}
}
return nil

Просмотреть файл

@ -34,10 +34,10 @@ func exists(path string) bool {
}
// crawlDoc fetches the package documentation from the VCS and updates the database.
func crawlDoc(source string, path string, pdoc *doc.Package, hasSubdirs bool, nextCrawl time.Time) (*doc.Package, error) {
func crawlDoc(source string, importPath string, pdoc *doc.Package, hasSubdirs bool, nextCrawl time.Time) (*doc.Package, error) {
message := []interface{}{source}
defer func() {
message = append(message, path)
message = append(message, importPath)
log.Println(message...)
}()
@ -56,49 +56,56 @@ func crawlDoc(source string, path string, pdoc *doc.Package, hasSubdirs bool, ne
start := time.Now()
var err error
if i := strings.Index(path, "/src/pkg/"); i > 0 && doc.IsGoRepoPath(path[i+len("/src/pkg/"):]) {
if i := strings.Index(importPath, "/src/pkg/"); i > 0 && doc.IsGoRepoPath(importPath[i+len("/src/pkg/"):]) {
// Go source tree mirror.
pdoc = nil
err = doc.NotFoundError{Message: "Go source tree mirror."}
} else if i := strings.Index(path, "/libgo/go/"); i > 0 && doc.IsGoRepoPath(path[i+len("/libgo/go/"):]) {
} else if i := strings.Index(importPath, "/libgo/go/"); i > 0 && doc.IsGoRepoPath(importPath[i+len("/libgo/go/"):]) {
// Go Frontend source tree mirror.
pdoc = nil
err = doc.NotFoundError{Message: "Go Frontend source tree mirror."}
} else if m := nestedProjectPat.FindStringIndex(path); m != nil && exists(path[m[0]+1:]) {
} else if m := nestedProjectPat.FindStringIndex(importPath); m != nil && exists(importPath[m[0]+1:]) {
pdoc = nil
err = doc.NotFoundError{Message: "Copy of other project."}
} else if blocked, e := db.IsBlocked(path); blocked && e == nil {
} else if blocked, e := db.IsBlocked(importPath); blocked && e == nil {
pdoc = nil
err = doc.NotFoundError{Message: "Blocked."}
} else {
var pdocNew *doc.Package
pdocNew, err = doc.Get(httpClient, path, etag)
pdocNew, err = doc.Get(httpClient, importPath, etag)
message = append(message, "fetch:", int64(time.Since(start)/time.Millisecond))
if err != doc.ErrNotModified {
if err == nil && pdocNew.Name == "" && !hasSubdirs {
pdoc = nil
err = doc.NotFoundError{Message: "No Go files or subdirs"}
} else if err != doc.ErrNotModified {
pdoc = pdocNew
}
}
nextCrawl = start.Add(*maxAge)
if strings.HasPrefix(path, "github.com/") || (pdoc != nil && len(pdoc.Errors) > 0) {
switch {
case strings.HasPrefix(importPath, "github.com/") || (pdoc != nil && len(pdoc.Errors) > 0):
nextCrawl = start.Add(*maxAge * 7)
case strings.HasPrefix(importPath, "gist.github.com/"):
// Don't spend time on gists. It's silly thing to do.
nextCrawl = start.Add(*maxAge * 30)
}
switch {
case err == nil:
message = append(message, "put:", pdoc.Etag)
if err := db.Put(pdoc, nextCrawl); err != nil {
log.Printf("ERROR db.Put(%q): %v", path, err)
log.Printf("ERROR db.Put(%q): %v", importPath, err)
}
case err == doc.ErrNotModified:
message = append(message, "touch")
if err := db.SetNextCrawlEtag(pdoc.ProjectRoot, pdoc.Etag, nextCrawl); err != nil {
log.Printf("ERROR db.SetNextCrawl(%q): %v", path, err)
log.Printf("ERROR db.SetNextCrawl(%q): %v", importPath, err)
}
case doc.IsNotFound(err):
message = append(message, "notfound:", err)
if err := db.Delete(path); err != nil {
log.Printf("ERROR db.Delete(%q): %v", path, err)
if err := db.Delete(importPath); err != nil {
log.Printf("ERROR db.Delete(%q): %v", importPath, err)
}
default:
message = append(message, "ERROR:", err)

Просмотреть файл

@ -130,11 +130,27 @@ func templateExt(req *web.Request) string {
}
var (
robotPat = regexp.MustCompile(`(:?\+https?://)|(?:\Wbot\W)`)
robotPat = regexp.MustCompile(`(:?\+https?://)|(?:\Wbot\W)|(?:^Python-urllib)|(?:^Go )|(?:^Java/)`)
)
func isRobot(req *web.Request) bool {
return *robot || robotPat.MatchString(req.Header.Get(web.HeaderUserAgent))
if robotPat.MatchString(req.Header.Get(web.HeaderUserAgent)) {
return true
}
host := req.RemoteAddr
if h, _, err := net.SplitHostPort(req.RemoteAddr); err == nil {
host = h
}
n, err := db.IncrementCounter(host, 1)
if err != nil {
log.Printf("error incrementing counter for %s, %v\n", host, err)
return false
}
if n > *robot {
log.Printf("robot %.2f %s %s", n, host, req.Header.Get(web.HeaderUserAgent))
return true
}
return false
}
func popularLinkReferral(req *web.Request) bool {
@ -300,7 +316,12 @@ func servePackage(resp web.Response, req *web.Request) error {
if err != nil {
return err
}
return executeTemplate(resp, "importers.html", web.StatusOK, nil, map[string]interface{}{
template := "importers.html"
if requestType == robotRequest {
// Hide back links from robots.
template = "importers_robot.html"
}
return executeTemplate(resp, template, web.StatusOK, nil, map[string]interface{}{
"pkgs": pkgs,
"pdoc": newTDoc(pdoc),
})
@ -736,7 +757,7 @@ func defaultBase(path string) string {
var (
db *database.Database
robot = flag.Bool("robot", false, "Robot mode")
robot = flag.Float64("robot", 100, "Request counter threshold for robots")
assetsDir = flag.String("assets", filepath.Join(defaultBase("github.com/garyburd/gddo/gddo-server"), "assets"), "Base directory for templates and static files.")
gzAssetsDir = flag.String("gzassets", "", "Base directory for compressed static files.")
presentDir = flag.String("present", defaultBase("code.google.com/p/go.talks/present"), "Base directory for templates and static files.")
@ -827,6 +848,7 @@ func main() {
{"dir.html", "common.html", "layout.html"},
{"home.html", "common.html", "layout.html"},
{"importers.html", "common.html", "layout.html"},
{"importers_robot.html", "common.html", "layout.html"},
{"imports.html", "common.html", "layout.html"},
{"file.html", "common.html", "layout.html"},
{"index.html", "common.html", "layout.html"},
@ -946,7 +968,10 @@ func main() {
return
}
defer listener.Close()
s := &server.Server{Listener: listener, Handler: h} // add logger
s := &server.Server{
Listener: listener,
Handler: web.ProxyHeaderHandler("X-Real-Ip", "X-Scheme", h),
}
err = s.Serve()
if err != nil {
log.Fatal("Server", err)

Просмотреть файл

@ -157,7 +157,8 @@ func (pdoc *tdoc) Breadcrumbs(templateName string) htemp.HTML {
if i != 0 {
buf.WriteString(`<span class="text-muted">/</span>`)
}
link := j < len(pdoc.ImportPath) || (templateName != "cmd.html" && templateName != "pkg.html")
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], ""))