package main
import (
"bytes"
"context"
//"errors"
"go/build"
"html/template"
"io/ioutil"
"log"
"net/http"
"os"
"os/exec"
"path/filepath"
"runtime"
"strings"
"sync"
"time"
)
type Go101 struct {
staticHandler http.Handler
isLocalServer bool
pageGroups map[string]*PageGroup
articlePages map[[2]string][]byte
serverMutex sync.Mutex
theme string
}
type PageGroup struct {
resHandler http.Handler
indexContent template.HTML
}
var go101 = &Go101{
staticHandler: http.StripPrefix("/static/", staticFilesHandler),
isLocalServer: false, // may be modified later
pageGroups: collectPageGroups(),
articlePages: map[[2]string][]byte{},
}
func init() {
for group, pg := range go101.pageGroups {
pg.indexContent = retrieveIndexContent(group)
}
}
func (go101 *Go101) ServeHTTP(w http.ResponseWriter, r *http.Request) {
var group, item string
if tokens := strings.SplitN(r.URL.Path[1:], "/", 2); len(tokens) == 2 {
group, item = tokens[0], tokens[1]
} else { // len(tokens) == 1
item = tokens[0]
}
switch go101.PreHandle(w, r); group {
case "":
if item == "" {
item = "index.html"
}
go101.serveGroupItem(w, r, "website", item)
case "res":
go101.serveGroupItem(w, r, "website", r.URL.Path[1:])
case "static":
w.Header().Set("Cache-Control", "max-age=31536000") // one year
go101.staticHandler.ServeHTTP(w, r)
case "article":
// for history reason, fundamentals pages use "article/xxx" URLs
go101.serveGroupItem(w, r, "fundamentals", item)
case "optimizations", "details-and-tips", "quizzes", "generics",
"apps-and-libs", "blog":
go101.serveGroupItem(w, r, group, item)
default:
http.Redirect(w, r, "/", http.StatusNotFound)
}
}
func (go101 *Go101) serveGroupItem(w http.ResponseWriter, r *http.Request, group, item string) {
item = strings.ToLower(item)
if strings.HasPrefix(item, "res/") {
w.Header().Set("Cache-Control", "max-age=31536000") // one year
go101.pageGroups[group].resHandler.ServeHTTP(w, r)
} else if !go101.RedirectArticlePage(w, r, group, item) {
go101.RenderArticlePage(w, r, group, item)
}
}
func (go101 *Go101) PreHandle(w http.ResponseWriter, r *http.Request) {
go101.serverMutex.Lock()
defer go101.serverMutex.Unlock()
localServer := isLocalRequest(r)
if go101.isLocalServer != localServer {
go101.isLocalServer = localServer
if go101.isLocalServer {
unloadPageTemplates() // loaded in one init function
go101.articlePages = map[[2]string][]byte{} // invalidate article caches
}
}
}
func (go101 *Go101) IsLocalServer() (isLocal bool) {
go101.serverMutex.Lock()
defer go101.serverMutex.Unlock()
isLocal = go101.isLocalServer
return
}
func pullGolang101Project(wd string) {
<-time.After(time.Minute / 2)
gitPull(wd)
for {
<-time.After(time.Hour * 24)
gitPull(wd)
}
}
func (go101 *Go101) ArticlePage(group, file string) ([]byte, bool) {
go101.serverMutex.Lock()
defer go101.serverMutex.Unlock()
page := go101.articlePages[[2]string{group, file}]
isLocal := go101.isLocalServer
return page, isLocal
}
func (go101 *Go101) CacheArticlePage(group, file string, page []byte) {
go101.serverMutex.Lock()
defer go101.serverMutex.Unlock()
go101.articlePages[[2]string{group, file}] = page
}
//===================================================
// pages
//==================================================
type Article struct {
Content, Title, Index template.HTML
TitleWithoutTags string
Group, Filename string
FilenameWithoutExt string
}
var schemes = map[bool]string{false: "http://", true: "https://"}
func (go101 *Go101) RenderArticlePage(w http.ResponseWriter, r *http.Request, group, file string) {
page, isLocal := go101.ArticlePage(group, file)
if page == nil {
article, err := retrieveArticleContent(group, file)
if err == nil {
article.Index = disableArticleLink(go101.pageGroups[group].indexContent, file)
pageParams := map[string]interface{}{
"Article": article,
"Title": article.TitleWithoutTags,
"Theme": go101.theme,
//"IsLocalServer": isLocal,
}
t := retrievePageTemplate(Template_Article, !isLocal)
var buf bytes.Buffer
if err = t.Execute(&buf, pageParams); err == nil {
page = buf.Bytes()
} else {
page = []byte(err.Error())
}
} else if os.IsNotExist(err) {
page = []byte{} // blank page means page not found.
}
if !isLocal {
go101.CacheArticlePage(group, file, page)
}
}
if len(page) == 0 { // blank page means page not found.
log.Printf("文章%s/%s未找到", group, file)
//w.Header().Set("Cache-Control", "no-cache, private, max-age=0")
http.Redirect(w, r, "/", http.StatusNotFound)
return
}
if isLocal {
w.Header().Set("Cache-Control", "no-cache, private, max-age=0")
} else {
w.Header().Set("Cache-Control", "max-age=50000") // about 14 hours
}
w.Write(page)
}
var H1, _H1 = []byte("
"), []byte("
")
var H2, _H2 = []byte(""), []byte("
")
const MaxTitleLen = 256
var TagSigns = [2]rune{'<', '>'}
func retrieveArticleContent(group, file string) (Article, error) {
article := Article{}
content, err := loadArticleFile(group, file)
if err != nil {
return article, err
}
article.Content = template.HTML(content)
article.Group = group
article.Filename = file
article.FilenameWithoutExt = strings.TrimSuffix(file, ".html")
// retrieve titles
splitTitleContent := func(startTag, endTag []byte) (int, int) {
j, i := -1, bytes.Index(content, startTag)
if i >= 0 {
i += len(startTag)
j = bytes.Index(bytesWithLength(content[i:], MaxTitleLen), endTag)
}
if j < 0 {
return -1, 0
}
return i - len(startTag), i + j + len(endTag)
}
titleStart, contentStart := splitTitleContent(H1, _H1)
if titleStart < 0 {
titleStart, contentStart = splitTitleContent(H2, _H2)
}
if titleStart < 0 {
//log.Println("retrieveTitlesForArticle failed:", group, file)
} else {
article.Title = article.Content[titleStart:contentStart]
article.Content = article.Content[contentStart:]
k, s := 0, make([]rune, 0, MaxTitleLen)
for _, r := range article.Title {
if r == TagSigns[k] {
k = (k + 1) & 1
} else if k == 0 {
s = append(s, r)
}
}
article.TitleWithoutTags = string(s)
}
return article, nil
}
func retrieveIndexContent(group string) template.HTML {
page101, err := retrieveArticleContent(group, "101.html")
if err != nil {
if os.IsNotExist(err) { // errors.Is(err, os.ErrNotExist) {
return ""
}
panic(err)
}
content := []byte(page101.Content)
start := []byte("")
i := bytes.Index(content, start)
if i < 0 {
//panic("index not found")
//log.Printf("index not found in %s/101/html", group)
return ""
}
content = content[i+len(start):]
end := []byte("")
i = bytes.Index(content, end)
if i < 0 {
//panic("index not found")
//log.Printf("index not found in %s/101/html", group)
return ""
}
content = content[:i]
//comments := [][]byte{
// []byte(""),
//}
//for _, cmt := range comments {
// i = bytes.Index(content, cmt)
// if i >= 0 {
// filleBytes(content[i:i+len(cmt)], ' ')
// }
//}
return template.HTML(content)
}
var (
aEnd = []byte(``)
aHref = []byte(`href="`)
aID = []byte(`id="i-`)
)
func disableArticleLink(htmlContent template.HTML, page string) (r template.HTML) {
content := []byte(htmlContent)
aStart := []byte(` NumPageTemplates {
which = NumPageTemplates
}
pageTemplatesMutex.Lock()
t := pageTemplates[which]
pageTemplatesMutex.Unlock()
if t == nil {
switch which {
case Template_Article:
t = parseTemplate(pageTemplatesCommonPaths, "article")
case Template_Redirect:
t = parseTemplate(pageTemplatesCommonPaths, "redirect")
default:
t = template.New("blank")
}
if cacheIt {
pageTemplatesMutex.Lock()
pageTemplates[which] = t
pageTemplatesMutex.Unlock()
}
}
return t
}
func unloadPageTemplates() {
pageTemplatesMutex.Lock()
defer pageTemplatesMutex.Unlock()
for i := range pageTemplates {
pageTemplates[i] = nil
}
}
//===================================================
// non-embedding functions
//===================================================
var dummyHandler http.Handler = http.HandlerFunc(func(http.ResponseWriter, *http.Request) {})
var staticFilesHandler_NonEmbedding = http.FileServer(http.Dir(filepath.Join(rootPath, "web", "static")))
func collectPageGroups_NonEmbedding() map[string]*PageGroup {
infos, err := ioutil.ReadDir(filepath.Join(rootPath, "pages"))
if err != nil {
panic("collect page groups error: " + err.Error())
}
pageGroups := make(map[string]*PageGroup, len(infos))
for _, e := range infos {
if e.IsDir() {
group, handler := e.Name(), dummyHandler
resPath := filepath.Join(rootPath, "pages", group, "res")
if _, err := os.Stat(resPath); err == nil {
var urlPrefix string
// For history reason, fundamentals pages uses "/article/xxx" URLs.
if group == "fundamentals" {
urlPrefix = "/article"
} else if group != "website" {
urlPrefix = "/" + group
}
handler = http.StripPrefix(urlPrefix+"/res/", http.FileServer(http.Dir(resPath)))
} else if !os.IsNotExist(err) { // !errors.Is(err, os.ErrNotExist) {
log.Println(err)
}
pageGroups[group] = &PageGroup{resHandler: handler}
}
}
return pageGroups
}
func loadArticleFile_NonEmbedding(group, file string) ([]byte, error) {
return ioutil.ReadFile(filepath.Join(rootPath, "pages", group, file))
}
func parseTemplate_NonEmbedding(commonPaths []string, files ...string) *template.Template {
cp := filepath.Join(commonPaths...)
ts := make([]string, len(files))
for i, f := range files {
ts[i] = filepath.Join(rootPath, cp, f)
}
return template.Must(template.ParseFiles(ts...))
}
func updateGolang101_NonEmbedding() {
pullGolang101Project(rootPath)
}
var rootPath, wdIsGo101ProjectRoot = findGo101ProjectRoot()
func findGo101ProjectRoot() (string, bool) {
if _, err := os.Stat(filepath.Join(".", "golang101.go")); err == nil {
return ".", true
}
for _, name := range []string{
"gitlab.com/golang101/golang101",
"gitlab.com/Golang101/golang101",
"github.com/golang101/golang101",
"github.com/Golang101/golang101",
} {
pkg, err := build.Import(name, "", build.FindOnly)
if err == nil {
return pkg.Dir, false
}
}
return ".", false
}
//===================================================
// utils
//===================================================
func bytesWithLength(s []byte, n int) []byte {
if n > len(s) {
n = len(s)
}
return s[:n]
}
func filleBytes(s []byte, b byte) {
for i := range s {
s[i] = b
}
}
func openBrowser(url string) error {
var cmd string
var args []string
switch runtime.GOOS {
case "windows":
cmd = "cmd"
args = []string{"/c", "start"}
case "darwin":
cmd = "open"
default: // "linux", "freebsd", "openbsd", "netbsd"
cmd = "xdg-open"
}
return exec.Command(cmd, append(args, url)...).Start()
}
func isLocalRequest(r *http.Request) bool {
end := strings.Index(r.Host, ":")
if end < 0 {
end = len(r.Host)
}
hostname := r.Host[:end]
return hostname == "localhost" // || hostname == "127.0.0.1" // 127.* for local cached version now
}
func runShellCommand(timeout time.Duration, wd string, cmd string, args ...string) ([]byte, error) {
ctx, cancel := context.WithTimeout(context.Background(), timeout)
defer cancel()
command := exec.CommandContext(ctx, cmd, args...)
command.Dir = wd
return command.Output()
}
func gitPull(wd string) {
output, err := runShellCommand(time.Minute/2, wd, "git", "pull")
if err != nil {
log.Println("git pull:", err)
} else {
log.Printf("git pull: %s", output)
}
}
func goGet(pkgPath, wd string) {
_, err := runShellCommand(time.Minute/2, wd, "go", "get", "-u", pkgPath)
if err != nil {
log.Println("go get -u "+pkgPath+":", err)
} else {
log.Println("go get -u " + pkgPath + " succeeded.")
}
}