serve.go 4.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177
  1. /*
  2. Copyright © 2022 Tone
  3. This program is free software: you can redistribute it and/or modify
  4. it under the terms of the GNU Affero General Public License as published by
  5. the Free Software Foundation, either version 3 of the License, or
  6. (at your option) any later version.
  7. This program is distributed in the hope that it will be useful,
  8. but WITHOUT ANY WARRANTY; without even the implied warranty of
  9. MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  10. GNU Affero General Public License for more details.
  11. You should have received a copy of the GNU Affero General Public License
  12. along with this program. If not, see <http://www.gnu.org/licenses/>.
  13. */
  14. package cmd
  15. import (
  16. "fmt"
  17. "html/template"
  18. "io/ioutil"
  19. "log"
  20. "net/http"
  21. "os"
  22. "path/filepath"
  23. "strings"
  24. "github.com/dustin/go-humanize"
  25. "github.com/spf13/cobra"
  26. "github.com/spf13/viper"
  27. "github.com/t0n3/sakuin/web"
  28. )
  29. type fileItem struct {
  30. Name string
  31. Size string
  32. Date string
  33. IsDir bool
  34. Path string
  35. }
  36. type templateVariables struct {
  37. Path []breadcrumb
  38. Files []fileItem
  39. }
  40. type breadcrumb struct {
  41. Name string
  42. Path string
  43. }
  44. var dataDir string
  45. // serveCmd represents the serve command
  46. var serveCmd = &cobra.Command{
  47. Use: "serve",
  48. Short: "Start the HTTP server",
  49. Run: func(cmd *cobra.Command, args []string) {
  50. dataDir = viper.GetString("data-dir")
  51. if dataDir == "" {
  52. log.Fatalln("Error: please specify a data directory, can't be empty")
  53. }
  54. _, err := os.Stat(dataDir)
  55. if err != nil {
  56. if os.IsNotExist(err) {
  57. log.Fatalln("Error: please specify a valid data directory")
  58. }
  59. }
  60. port := viper.GetInt("port")
  61. address := viper.GetString("listen-addr")
  62. mux := http.NewServeMux()
  63. mux.Handle("/assets/", web.AssetsHandler("/assets/", "dist"))
  64. mux.HandleFunc("/", serve)
  65. log.Printf("Starting Sakuin HTTP Server on %s:%d\n", address, port)
  66. http.ListenAndServe(fmt.Sprintf("%s:%d", address, port), mux)
  67. },
  68. }
  69. func init() {
  70. rootCmd.AddCommand(serveCmd)
  71. serveCmd.Flags().StringP("data-dir", "d", "", "Directory containing data that Sakuin will serve")
  72. serveCmd.Flags().IntP("port", "p", 3000, "Port to listen to")
  73. serveCmd.Flags().String("listen-addr", "0.0.0.0", "Address to listen to")
  74. viper.BindPFlag("data-dir", serveCmd.Flags().Lookup("data-dir"))
  75. viper.BindPFlag("port", serveCmd.Flags().Lookup("port"))
  76. viper.BindPFlag("listen-addr", serveCmd.Flags().Lookup("listen-addr"))
  77. }
  78. func serve(w http.ResponseWriter, r *http.Request) {
  79. // Filepath, from the root data dir
  80. fp := filepath.Join(dataDir, filepath.Clean(r.URL.Path))
  81. // Cleaned filepath, without the root data dir, used for template rendering purpose
  82. cfp := strings.Replace(fp, dataDir, "", 1)
  83. // Return a 404 if the template doesn't exist
  84. info, err := os.Stat(fp)
  85. if err != nil {
  86. if os.IsNotExist(err) {
  87. notFound, _ := template.ParseFS(web.NotFound, "404.html")
  88. w.WriteHeader(http.StatusNotFound)
  89. notFound.ExecuteTemplate(w, "404.html", nil)
  90. log.Printf("404 - %s\n", cfp)
  91. return
  92. }
  93. }
  94. // Return a 404 if the request is for a directory
  95. if info.IsDir() {
  96. files, err := ioutil.ReadDir(fp)
  97. if err != nil {
  98. log.Fatal(err)
  99. }
  100. // Init template variables
  101. templateVars := templateVariables{}
  102. // Construct the breadcrumb
  103. path := strings.Split(cfp, "/")
  104. for len(path) > 1 {
  105. b := breadcrumb{
  106. Name: path[len(path)-1],
  107. Path: strings.Join(path, "/"),
  108. }
  109. path = path[:len(path)-1]
  110. templateVars.Path = append(templateVars.Path, b)
  111. }
  112. // Since the breadcrumb built is not very ordered...
  113. // REVERSE ALL THE THINGS
  114. for left, right := 0, len(templateVars.Path)-1; left < right; left, right = left+1, right-1 {
  115. templateVars.Path[left], templateVars.Path[right] = templateVars.Path[right], templateVars.Path[left]
  116. }
  117. // Establish list of files in the current directory
  118. for _, f := range files {
  119. templateVars.Files = append(templateVars.Files, fileItem{
  120. Name: f.Name(),
  121. Size: humanize.Bytes(uint64(f.Size())),
  122. Date: humanize.Time(f.ModTime()),
  123. IsDir: f.IsDir(),
  124. Path: filepath.Join(cfp, filepath.Clean(f.Name())),
  125. })
  126. }
  127. // Prepare the template
  128. tmpl, err := template.ParseFS(web.Index, "index.html")
  129. if err != nil {
  130. // Log the detailed error
  131. log.Println(err.Error())
  132. // Return a generic "Internal Server Error" message
  133. http.Error(w, http.StatusText(500), 500)
  134. return
  135. }
  136. // Return file listing in the template
  137. if err := tmpl.ExecuteTemplate(w, "index.html", templateVars); err != nil {
  138. log.Println(err.Error())
  139. http.Error(w, http.StatusText(500), 500)
  140. }
  141. log.Printf("200 - DIR %s\n", "/")
  142. return
  143. }
  144. if !info.IsDir() {
  145. content, _ := os.Open(fp)
  146. w.Header().Add("Content-Disposition", fmt.Sprintf("attachment; filename=\"%s\"", info.Name()))
  147. http.ServeContent(w, r, fp, info.ModTime(), content)
  148. log.Printf("200 - FILE %s\n", cfp)
  149. return
  150. }
  151. }