golang101.go 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511
  1. package main
  2. import (
  3. "bytes"
  4. "context"
  5. //"errors"
  6. "go/build"
  7. "html/template"
  8. "io/ioutil"
  9. "log"
  10. "net/http"
  11. "os"
  12. "os/exec"
  13. "path/filepath"
  14. "runtime"
  15. "strings"
  16. "sync"
  17. "time"
  18. )
  19. type Go101 struct {
  20. staticHandler http.Handler
  21. isLocalServer bool
  22. pageGroups map[string]*PageGroup
  23. articlePages map[[2]string][]byte
  24. serverMutex sync.Mutex
  25. theme string
  26. }
  27. type PageGroup struct {
  28. resHandler http.Handler
  29. indexContent template.HTML
  30. }
  31. var go101 = &Go101{
  32. staticHandler: http.StripPrefix("/static/", staticFilesHandler),
  33. isLocalServer: false, // may be modified later
  34. pageGroups: collectPageGroups(),
  35. articlePages: map[[2]string][]byte{},
  36. }
  37. func init() {
  38. for group, pg := range go101.pageGroups {
  39. pg.indexContent = retrieveIndexContent(group)
  40. }
  41. }
  42. func (go101 *Go101) ServeHTTP(w http.ResponseWriter, r *http.Request) {
  43. var group, item string
  44. if tokens := strings.SplitN(r.URL.Path[1:], "/", 2); len(tokens) == 2 {
  45. group, item = tokens[0], tokens[1]
  46. } else { // len(tokens) == 1
  47. item = tokens[0]
  48. }
  49. switch go101.PreHandle(w, r); group {
  50. case "":
  51. if item == "" {
  52. item = "index.html"
  53. }
  54. go101.serveGroupItem(w, r, "website", item)
  55. case "res":
  56. go101.serveGroupItem(w, r, "website", r.URL.Path[1:])
  57. case "static":
  58. w.Header().Set("Cache-Control", "max-age=31536000") // one year
  59. go101.staticHandler.ServeHTTP(w, r)
  60. case "article":
  61. // for history reason, fundamentals pages use "article/xxx" URLs
  62. go101.serveGroupItem(w, r, "fundamentals", item)
  63. case "optimizations", "details-and-tips", "quizzes", "generics",
  64. "apps-and-libs", "blog":
  65. go101.serveGroupItem(w, r, group, item)
  66. default:
  67. http.Redirect(w, r, "/", http.StatusNotFound)
  68. }
  69. }
  70. func (go101 *Go101) serveGroupItem(w http.ResponseWriter, r *http.Request, group, item string) {
  71. item = strings.ToLower(item)
  72. if strings.HasPrefix(item, "res/") {
  73. w.Header().Set("Cache-Control", "max-age=31536000") // one year
  74. go101.pageGroups[group].resHandler.ServeHTTP(w, r)
  75. } else if !go101.RedirectArticlePage(w, r, group, item) {
  76. go101.RenderArticlePage(w, r, group, item)
  77. }
  78. }
  79. func (go101 *Go101) PreHandle(w http.ResponseWriter, r *http.Request) {
  80. go101.serverMutex.Lock()
  81. defer go101.serverMutex.Unlock()
  82. localServer := isLocalRequest(r)
  83. if go101.isLocalServer != localServer {
  84. go101.isLocalServer = localServer
  85. if go101.isLocalServer {
  86. unloadPageTemplates() // loaded in one init function
  87. go101.articlePages = map[[2]string][]byte{} // invalidate article caches
  88. }
  89. }
  90. }
  91. func (go101 *Go101) IsLocalServer() (isLocal bool) {
  92. go101.serverMutex.Lock()
  93. defer go101.serverMutex.Unlock()
  94. isLocal = go101.isLocalServer
  95. return
  96. }
  97. func pullGolang101Project(wd string) {
  98. <-time.After(time.Minute / 2)
  99. gitPull(wd)
  100. for {
  101. <-time.After(time.Hour * 24)
  102. gitPull(wd)
  103. }
  104. }
  105. func (go101 *Go101) ArticlePage(group, file string) ([]byte, bool) {
  106. go101.serverMutex.Lock()
  107. defer go101.serverMutex.Unlock()
  108. page := go101.articlePages[[2]string{group, file}]
  109. isLocal := go101.isLocalServer
  110. return page, isLocal
  111. }
  112. func (go101 *Go101) CacheArticlePage(group, file string, page []byte) {
  113. go101.serverMutex.Lock()
  114. defer go101.serverMutex.Unlock()
  115. go101.articlePages[[2]string{group, file}] = page
  116. }
  117. //===================================================
  118. // pages
  119. //==================================================
  120. type Article struct {
  121. Content, Title, Index template.HTML
  122. TitleWithoutTags string
  123. Group, Filename string
  124. FilenameWithoutExt string
  125. }
  126. var schemes = map[bool]string{false: "http://", true: "https://"}
  127. func (go101 *Go101) RenderArticlePage(w http.ResponseWriter, r *http.Request, group, file string) {
  128. page, isLocal := go101.ArticlePage(group, file)
  129. if page == nil {
  130. article, err := retrieveArticleContent(group, file)
  131. if err == nil {
  132. article.Index = disableArticleLink(go101.pageGroups[group].indexContent, file)
  133. pageParams := map[string]interface{}{
  134. "Article": article,
  135. "Title": article.TitleWithoutTags,
  136. "Theme": go101.theme,
  137. //"IsLocalServer": isLocal,
  138. }
  139. t := retrievePageTemplate(Template_Article, !isLocal)
  140. var buf bytes.Buffer
  141. if err = t.Execute(&buf, pageParams); err == nil {
  142. page = buf.Bytes()
  143. } else {
  144. page = []byte(err.Error())
  145. }
  146. } else if os.IsNotExist(err) {
  147. page = []byte{} // blank page means page not found.
  148. }
  149. if !isLocal {
  150. go101.CacheArticlePage(group, file, page)
  151. }
  152. }
  153. if len(page) == 0 { // blank page means page not found.
  154. log.Printf("文章%s/%s未找到", group, file)
  155. //w.Header().Set("Cache-Control", "no-cache, private, max-age=0")
  156. http.Redirect(w, r, "/", http.StatusNotFound)
  157. return
  158. }
  159. if isLocal {
  160. w.Header().Set("Cache-Control", "no-cache, private, max-age=0")
  161. } else {
  162. w.Header().Set("Cache-Control", "max-age=50000") // about 14 hours
  163. }
  164. w.Write(page)
  165. }
  166. var H1, _H1 = []byte("<h1>"), []byte("</h1>")
  167. var H2, _H2 = []byte("<h2>"), []byte("</h2>")
  168. const MaxTitleLen = 256
  169. var TagSigns = [2]rune{'<', '>'}
  170. func retrieveArticleContent(group, file string) (Article, error) {
  171. article := Article{}
  172. content, err := loadArticleFile(group, file)
  173. if err != nil {
  174. return article, err
  175. }
  176. article.Content = template.HTML(content)
  177. article.Group = group
  178. article.Filename = file
  179. article.FilenameWithoutExt = strings.TrimSuffix(file, ".html")
  180. // retrieve titles
  181. splitTitleContent := func(startTag, endTag []byte) (int, int) {
  182. j, i := -1, bytes.Index(content, startTag)
  183. if i >= 0 {
  184. i += len(startTag)
  185. j = bytes.Index(bytesWithLength(content[i:], MaxTitleLen), endTag)
  186. }
  187. if j < 0 {
  188. return -1, 0
  189. }
  190. return i - len(startTag), i + j + len(endTag)
  191. }
  192. titleStart, contentStart := splitTitleContent(H1, _H1)
  193. if titleStart < 0 {
  194. titleStart, contentStart = splitTitleContent(H2, _H2)
  195. }
  196. if titleStart < 0 {
  197. //log.Println("retrieveTitlesForArticle failed:", group, file)
  198. } else {
  199. article.Title = article.Content[titleStart:contentStart]
  200. article.Content = article.Content[contentStart:]
  201. k, s := 0, make([]rune, 0, MaxTitleLen)
  202. for _, r := range article.Title {
  203. if r == TagSigns[k] {
  204. k = (k + 1) & 1
  205. } else if k == 0 {
  206. s = append(s, r)
  207. }
  208. }
  209. article.TitleWithoutTags = string(s)
  210. }
  211. return article, nil
  212. }
  213. func retrieveIndexContent(group string) template.HTML {
  214. page101, err := retrieveArticleContent(group, "101.html")
  215. if err != nil {
  216. if os.IsNotExist(err) { // errors.Is(err, os.ErrNotExist) {
  217. return ""
  218. }
  219. panic(err)
  220. }
  221. content := []byte(page101.Content)
  222. start := []byte("<!-- index starts (don't remove) -->")
  223. i := bytes.Index(content, start)
  224. if i < 0 {
  225. //panic("index not found")
  226. //log.Printf("index not found in %s/101/html", group)
  227. return ""
  228. }
  229. content = content[i+len(start):]
  230. end := []byte("<!-- index ends (don't remove) -->")
  231. i = bytes.Index(content, end)
  232. if i < 0 {
  233. //panic("index not found")
  234. //log.Printf("index not found in %s/101/html", group)
  235. return ""
  236. }
  237. content = content[:i]
  238. //comments := [][]byte{
  239. // []byte("<!-- (to remove) for printing"),
  240. // []byte("(to remove) -->"),
  241. //}
  242. //for _, cmt := range comments {
  243. // i = bytes.Index(content, cmt)
  244. // if i >= 0 {
  245. // filleBytes(content[i:i+len(cmt)], ' ')
  246. // }
  247. //}
  248. return template.HTML(content)
  249. }
  250. var (
  251. aEnd = []byte(`</a>`)
  252. aHref = []byte(`href="`)
  253. aID = []byte(`id="i-`)
  254. )
  255. func disableArticleLink(htmlContent template.HTML, page string) (r template.HTML) {
  256. content := []byte(htmlContent)
  257. aStart := []byte(`<a class="index" href="` + page)
  258. i := bytes.Index(content, aStart)
  259. if i >= 0 {
  260. content := content[i:]
  261. i = bytes.Index(content[len(aStart):], aEnd)
  262. if i >= 0 {
  263. i += len(aStart)
  264. //filleBytes(content[:len(start)], 0)
  265. //filleBytes(content[i:i+len(end)], 0)
  266. k := bytes.Index(content, aHref)
  267. if i >= 0 {
  268. content[1] = 'b'
  269. content[i+2] = 'b'
  270. copy(content[k:], aID)
  271. }
  272. }
  273. }
  274. return template.HTML(content)
  275. }
  276. //===================================================
  277. // templates
  278. //==================================================
  279. type PageTemplate uint
  280. const (
  281. Template_Article PageTemplate = iota
  282. Template_Redirect
  283. NumPageTemplates
  284. )
  285. var pageTemplates [NumPageTemplates + 1]*template.Template
  286. var pageTemplatesMutex sync.Mutex //
  287. var pageTemplatesCommonPaths = []string{"web", "templates"}
  288. func init() {
  289. for i := range pageTemplates {
  290. retrievePageTemplate(PageTemplate(i), true)
  291. }
  292. }
  293. func retrievePageTemplate(which PageTemplate, cacheIt bool) *template.Template {
  294. if which > NumPageTemplates {
  295. which = NumPageTemplates
  296. }
  297. pageTemplatesMutex.Lock()
  298. t := pageTemplates[which]
  299. pageTemplatesMutex.Unlock()
  300. if t == nil {
  301. switch which {
  302. case Template_Article:
  303. t = parseTemplate(pageTemplatesCommonPaths, "article")
  304. case Template_Redirect:
  305. t = parseTemplate(pageTemplatesCommonPaths, "redirect")
  306. default:
  307. t = template.New("blank")
  308. }
  309. if cacheIt {
  310. pageTemplatesMutex.Lock()
  311. pageTemplates[which] = t
  312. pageTemplatesMutex.Unlock()
  313. }
  314. }
  315. return t
  316. }
  317. func unloadPageTemplates() {
  318. pageTemplatesMutex.Lock()
  319. defer pageTemplatesMutex.Unlock()
  320. for i := range pageTemplates {
  321. pageTemplates[i] = nil
  322. }
  323. }
  324. //===================================================
  325. // non-embedding functions
  326. //===================================================
  327. var dummyHandler http.Handler = http.HandlerFunc(func(http.ResponseWriter, *http.Request) {})
  328. var staticFilesHandler_NonEmbedding = http.FileServer(http.Dir(filepath.Join(rootPath, "web", "static")))
  329. func collectPageGroups_NonEmbedding() map[string]*PageGroup {
  330. infos, err := ioutil.ReadDir(filepath.Join(rootPath, "pages"))
  331. if err != nil {
  332. panic("collect page groups error: " + err.Error())
  333. }
  334. pageGroups := make(map[string]*PageGroup, len(infos))
  335. for _, e := range infos {
  336. if e.IsDir() {
  337. group, handler := e.Name(), dummyHandler
  338. resPath := filepath.Join(rootPath, "pages", group, "res")
  339. if _, err := os.Stat(resPath); err == nil {
  340. var urlPrefix string
  341. // For history reason, fundamentals pages uses "/article/xxx" URLs.
  342. if group == "fundamentals" {
  343. urlPrefix = "/article"
  344. } else if group != "website" {
  345. urlPrefix = "/" + group
  346. }
  347. handler = http.StripPrefix(urlPrefix+"/res/", http.FileServer(http.Dir(resPath)))
  348. } else if !os.IsNotExist(err) { // !errors.Is(err, os.ErrNotExist) {
  349. log.Println(err)
  350. }
  351. pageGroups[group] = &PageGroup{resHandler: handler}
  352. }
  353. }
  354. return pageGroups
  355. }
  356. func loadArticleFile_NonEmbedding(group, file string) ([]byte, error) {
  357. return ioutil.ReadFile(filepath.Join(rootPath, "pages", group, file))
  358. }
  359. func parseTemplate_NonEmbedding(commonPaths []string, files ...string) *template.Template {
  360. cp := filepath.Join(commonPaths...)
  361. ts := make([]string, len(files))
  362. for i, f := range files {
  363. ts[i] = filepath.Join(rootPath, cp, f)
  364. }
  365. return template.Must(template.ParseFiles(ts...))
  366. }
  367. func updateGolang101_NonEmbedding() {
  368. pullGolang101Project(rootPath)
  369. }
  370. var rootPath, wdIsGo101ProjectRoot = findGo101ProjectRoot()
  371. func findGo101ProjectRoot() (string, bool) {
  372. if _, err := os.Stat(filepath.Join(".", "golang101.go")); err == nil {
  373. return ".", true
  374. }
  375. for _, name := range []string{
  376. "gitlab.com/golang101/golang101",
  377. "gitlab.com/Golang101/golang101",
  378. "github.com/golang101/golang101",
  379. "github.com/Golang101/golang101",
  380. } {
  381. pkg, err := build.Import(name, "", build.FindOnly)
  382. if err == nil {
  383. return pkg.Dir, false
  384. }
  385. }
  386. return ".", false
  387. }
  388. //===================================================
  389. // utils
  390. //===================================================
  391. func bytesWithLength(s []byte, n int) []byte {
  392. if n > len(s) {
  393. n = len(s)
  394. }
  395. return s[:n]
  396. }
  397. func filleBytes(s []byte, b byte) {
  398. for i := range s {
  399. s[i] = b
  400. }
  401. }
  402. func openBrowser(url string) error {
  403. var cmd string
  404. var args []string
  405. switch runtime.GOOS {
  406. case "windows":
  407. cmd = "cmd"
  408. args = []string{"/c", "start"}
  409. case "darwin":
  410. cmd = "open"
  411. default: // "linux", "freebsd", "openbsd", "netbsd"
  412. cmd = "xdg-open"
  413. }
  414. return exec.Command(cmd, append(args, url)...).Start()
  415. }
  416. func isLocalRequest(r *http.Request) bool {
  417. end := strings.Index(r.Host, ":")
  418. if end < 0 {
  419. end = len(r.Host)
  420. }
  421. hostname := r.Host[:end]
  422. return hostname == "localhost" // || hostname == "127.0.0.1" // 127.* for local cached version now
  423. }
  424. func runShellCommand(timeout time.Duration, wd string, cmd string, args ...string) ([]byte, error) {
  425. ctx, cancel := context.WithTimeout(context.Background(), timeout)
  426. defer cancel()
  427. command := exec.CommandContext(ctx, cmd, args...)
  428. command.Dir = wd
  429. return command.Output()
  430. }
  431. func gitPull(wd string) {
  432. output, err := runShellCommand(time.Minute/2, wd, "git", "pull")
  433. if err != nil {
  434. log.Println("git pull:", err)
  435. } else {
  436. log.Printf("git pull: %s", output)
  437. }
  438. }
  439. func goGet(pkgPath, wd string) {
  440. _, err := runShellCommand(time.Minute/2, wd, "go", "get", "-u", pkgPath)
  441. if err != nil {
  442. log.Println("go get -u "+pkgPath+":", err)
  443. } else {
  444. log.Println("go get -u " + pkgPath + " succeeded.")
  445. }
  446. }