123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511 |
- 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("<h1>"), []byte("</h1>")
- var H2, _H2 = []byte("<h2>"), []byte("</h2>")
- 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("<!-- index starts (don't remove) -->")
- 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("<!-- index ends (don't remove) -->")
- 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("<!-- (to remove) for printing"),
- // []byte("(to remove) -->"),
- //}
- //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(`</a>`)
- aHref = []byte(`href="`)
- aID = []byte(`id="i-`)
- )
- func disableArticleLink(htmlContent template.HTML, page string) (r template.HTML) {
- content := []byte(htmlContent)
- aStart := []byte(`<a class="index" href="` + page)
- i := bytes.Index(content, aStart)
- if i >= 0 {
- content := content[i:]
- i = bytes.Index(content[len(aStart):], aEnd)
- if i >= 0 {
- i += len(aStart)
- //filleBytes(content[:len(start)], 0)
- //filleBytes(content[i:i+len(end)], 0)
- k := bytes.Index(content, aHref)
- if i >= 0 {
- content[1] = 'b'
- content[i+2] = 'b'
- copy(content[k:], aID)
- }
- }
- }
- return template.HTML(content)
- }
- //===================================================
- // templates
- //==================================================
- type PageTemplate uint
- const (
- Template_Article PageTemplate = iota
- Template_Redirect
- NumPageTemplates
- )
- var pageTemplates [NumPageTemplates + 1]*template.Template
- var pageTemplatesMutex sync.Mutex //
- var pageTemplatesCommonPaths = []string{"web", "templates"}
- func init() {
- for i := range pageTemplates {
- retrievePageTemplate(PageTemplate(i), true)
- }
- }
- func retrievePageTemplate(which PageTemplate, cacheIt bool) *template.Template {
- if which > 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.")
- }
- }
|