static.go 4.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141
  1. package engine
  2. // This source file is for the special case of serving a single file.
  3. import (
  4. "errors"
  5. "net/http"
  6. "strconv"
  7. "strings"
  8. "time"
  9. "github.com/gomarkdown/markdown"
  10. "github.com/gomarkdown/markdown/parser"
  11. log "github.com/sirupsen/logrus"
  12. "github.com/xyproto/algernon/utils"
  13. "github.com/xyproto/datablock"
  14. )
  15. const (
  16. defaultStaticCacheSize = 128 * utils.MiB
  17. maxAttemptsAtIncreasingPortNumber = 128
  18. delayBeforeLaunchingBrowser = time.Millisecond * 200
  19. )
  20. // nextPort increases the port number by 1
  21. func nextPort(colonPort string) (string, error) {
  22. if !strings.HasPrefix(colonPort, ":") {
  23. return colonPort, errors.New("colonPort does not start with a colon! \"" + colonPort + "\"")
  24. }
  25. num, err := strconv.Atoi(colonPort[1:])
  26. if err != nil {
  27. return colonPort, errors.New("Could not convert port number to string: \"" + colonPort[1:] + "\"")
  28. }
  29. // Increase the port number by 1, add a colon, convert to string and return
  30. return ":" + strconv.Itoa(num+1), nil
  31. }
  32. // This is a bit hacky, but it's only used when serving a single static file
  33. func (ac *Config) openAfter(wait time.Duration, hostname, colonPort string, https bool, cancelChannel chan bool) {
  34. // Wait a bit
  35. time.Sleep(wait)
  36. select {
  37. case <-cancelChannel:
  38. // Got a message on the cancelChannel:
  39. // don't open the URL with an external application.
  40. return
  41. case <-time.After(delayBeforeLaunchingBrowser):
  42. // Got timeout, assume the port was not busy
  43. ac.OpenURL(hostname, colonPort, https)
  44. }
  45. }
  46. // shortInfo outputs a short string about which file is served where
  47. func (ac *Config) shortInfoAndOpen(filename, colonPort string, cancelChannel chan bool) {
  48. hostname := "localhost"
  49. if ac.serverHost != "" {
  50. hostname = ac.serverHost
  51. }
  52. log.Info("Serving " + filename + " on http://" + hostname + colonPort)
  53. if ac.openURLAfterServing {
  54. go ac.openAfter(delayBeforeLaunchingBrowser, hostname, colonPort, false, cancelChannel)
  55. }
  56. }
  57. // ServeStaticFile is a convenience function for serving only a single file.
  58. // It can be used as a quick and easy way to view a README.md file.
  59. // Will also serve local images if the resulting HTML contains them.
  60. func (ac *Config) ServeStaticFile(filename, colonPort string) error {
  61. log.Info("Single file mode. Not using the regular parameters.")
  62. cancelChannel := make(chan bool, 1)
  63. ac.shortInfoAndOpen(filename, colonPort, cancelChannel)
  64. mux := http.NewServeMux()
  65. // 64 MiB cache, use cache compression, no per-file size limit, use best gzip compression, compress for size not for speed
  66. ac.cache = datablock.NewFileCache(defaultStaticCacheSize, true, 0, false, 0)
  67. if ac.markdownMode {
  68. // Discover all local images mentioned in the Markdown document
  69. var localImages []string
  70. if markdownData, err := ac.cache.Read(filename, true); err == nil { // success
  71. // Create a Markdown parser with the desired extensions
  72. extensions := parser.CommonExtensions | parser.AutoHeadingIDs
  73. mdParser := parser.NewWithExtensions(extensions)
  74. // Convert from Markdown to HTML
  75. htmlbody := markdown.ToHTML(markdownData.Bytes(), mdParser, nil)
  76. localImages = utils.ExtractLocalImagePaths(string(htmlbody))
  77. }
  78. // Serve all local images mentioned in the Markdown document.
  79. // If a file is not found, then the FilePage function will handle it.
  80. for _, localImage := range localImages {
  81. mux.HandleFunc("/"+localImage, func(w http.ResponseWriter, req *http.Request) {
  82. w.Header().Set("Server", ac.versionString)
  83. ac.FilePage(w, req, localImage, ac.defaultLuaDataFilename)
  84. })
  85. }
  86. }
  87. // Prepare to serve the given filename
  88. mux.HandleFunc("/", func(w http.ResponseWriter, req *http.Request) {
  89. w.Header().Set("Server", ac.versionString)
  90. ac.FilePage(w, req, filename, ac.defaultLuaDataFilename)
  91. })
  92. HTTPserver := ac.NewGracefulServer(mux, false, ac.serverHost+colonPort)
  93. // Attempt to serve the handler functions above
  94. if errServe := HTTPserver.ListenAndServe(); errServe != nil {
  95. // If it fails, try several times, increasing the port by 1 each time
  96. for i := 0; i < maxAttemptsAtIncreasingPortNumber; i++ {
  97. if errServe = HTTPserver.ListenAndServe(); errServe != nil {
  98. cancelChannel <- true
  99. if !strings.HasSuffix(errServe.Error(), "already in use") {
  100. // Not a problem with address already being in use
  101. ac.fatalExit(errServe)
  102. }
  103. log.Warn("Address already in use. Using next port number.")
  104. if newPort, errNext := nextPort(colonPort); errNext != nil {
  105. ac.fatalExit(errNext)
  106. } else {
  107. colonPort = newPort
  108. }
  109. // Make a new cancel channel, and use the new URL
  110. cancelChannel = make(chan bool, 1)
  111. ac.shortInfoAndOpen(filename, colonPort, cancelChannel)
  112. HTTPserver = ac.NewGracefulServer(mux, false, ac.serverHost+colonPort)
  113. }
  114. }
  115. // Several attempts failed
  116. return errServe
  117. }
  118. return nil
  119. }