main.go 3.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112
  1. // Command rekopub-web runs the converter behind the form on
  2. // rekopub.monoexp.com. It listens on 127.0.0.1 only, behind nginx, and runs
  3. // the rekopub command for each conversion.
  4. //
  5. // Settings come from the environment:
  6. //
  7. // PORT port to listen on (required)
  8. // REKOPUB the rekopub command (default: rekopub next to this program)
  9. // DATA_DIR writable folder; books are kept in DATA_DIR/books (default: data)
  10. // KEEP_HOURS hours a book is kept and its link works (default: 24)
  11. // LIMIT conversions for all visitors together... (default: 50)
  12. // LIMIT_PER_VISITOR ...and per visitor... (default: 10)
  13. // LIMIT_MINUTES ...within this many minutes (default: 15)
  14. // MAX_RUNNING conversions running at once (default: 2)
  15. // MAX_STORAGE_MB disk used by kept books before conversions pause (default: 2048)
  16. package main
  17. import (
  18. "context"
  19. "errors"
  20. "fmt"
  21. "log"
  22. "net/http"
  23. "os"
  24. "os/signal"
  25. "path/filepath"
  26. "strconv"
  27. "syscall"
  28. "time"
  29. "git.monoexp.com/bajdev/rekopub/internal/web"
  30. )
  31. func main() {
  32. log.SetFlags(0) // the journal adds timestamps
  33. port, cfg, err := config()
  34. if err != nil {
  35. log.Fatal(err)
  36. }
  37. srv, err := web.New(cfg)
  38. if err != nil {
  39. log.Fatal(err)
  40. }
  41. ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
  42. defer stop()
  43. go srv.CleanEvery(ctx, 10*time.Minute)
  44. hs := &http.Server{
  45. Addr: "127.0.0.1:" + port,
  46. Handler: srv.Handler(),
  47. ReadHeaderTimeout: 10 * time.Second,
  48. ReadTimeout: 30 * time.Second,
  49. IdleTimeout: 60 * time.Second,
  50. // No WriteTimeout: a conversion can take minutes. Each run of the
  51. // command has its own time limit instead.
  52. }
  53. go func() {
  54. <-ctx.Done()
  55. shutdown, cancel := context.WithTimeout(context.Background(), 10*time.Second)
  56. defer cancel()
  57. hs.Shutdown(shutdown)
  58. }()
  59. log.Printf("rekopub-web listening on %s, running %s", hs.Addr, cfg.CLI)
  60. if err := hs.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) {
  61. log.Fatal(err)
  62. }
  63. }
  64. func config() (string, web.Config, error) {
  65. var cfg web.Config
  66. port := os.Getenv("PORT")
  67. if _, err := strconv.ParseUint(port, 10, 16); err != nil {
  68. return "", cfg, fmt.Errorf("PORT %q: must be a port number", port)
  69. }
  70. cfg.CLI = os.Getenv("REKOPUB")
  71. if cfg.CLI == "" {
  72. exe, err := os.Executable()
  73. if err != nil {
  74. return "", cfg, err
  75. }
  76. cfg.CLI = filepath.Join(filepath.Dir(exe), "rekopub")
  77. }
  78. if _, err := os.Stat(cfg.CLI); err != nil {
  79. return "", cfg, fmt.Errorf("REKOPUB: %w", err)
  80. }
  81. data := os.Getenv("DATA_DIR")
  82. if data == "" {
  83. data = "data"
  84. }
  85. cfg.Dir = filepath.Join(data, "books")
  86. var err error
  87. num := func(name string) int {
  88. v := os.Getenv(name)
  89. if v == "" || err != nil {
  90. return 0 // the default
  91. }
  92. n, perr := strconv.Atoi(v)
  93. if perr != nil || n <= 0 {
  94. err = fmt.Errorf("%s %q: must be a whole number above 0", name, v)
  95. }
  96. return n
  97. }
  98. cfg.Keep = time.Duration(num("KEEP_HOURS")) * time.Hour
  99. cfg.Limit = num("LIMIT")
  100. cfg.PerVisitor = num("LIMIT_PER_VISITOR")
  101. cfg.Window = time.Duration(num("LIMIT_MINUTES")) * time.Minute
  102. cfg.MaxRunning = num("MAX_RUNNING")
  103. cfg.MaxStorage = int64(num("MAX_STORAGE_MB")) << 20
  104. return port, cfg, err
  105. }