Small HTTP server that redirects to artifacts from the latest github release for a project
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

main.go 3.7KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161
  1. package main
  2. import (
  3. "context"
  4. "flag"
  5. "fmt"
  6. "github.com/google/go-github/github"
  7. "log"
  8. "net/http"
  9. "os"
  10. "os/signal"
  11. "strings"
  12. "syscall"
  13. "time"
  14. )
  15. var (
  16. owner string
  17. repo string
  18. redirect *string
  19. webhookPath *string
  20. ctx context.Context
  21. client *github.Client
  22. release *github.RepositoryRelease
  23. ticker *time.Ticker
  24. )
  25. func fetchLatest() {
  26. latest, _, err := client.Repositories.GetLatestRelease(ctx, owner, repo)
  27. if err != nil {
  28. log.Println("Error retrieving latest release", err)
  29. return
  30. }
  31. log.Printf("Found latest release: %s\n", *latest.Name)
  32. release = latest
  33. }
  34. func temporaryRedirect(w http.ResponseWriter, url string) {
  35. w.Header().Add("Location", url)
  36. w.WriteHeader(http.StatusTemporaryRedirect)
  37. }
  38. func serveStaticPaths(w http.ResponseWriter, request *http.Request) bool {
  39. if "/" == request.RequestURI && len(*redirect) > 0 {
  40. temporaryRedirect(w, *redirect)
  41. return true
  42. }
  43. if *webhookPath == request.RequestURI {
  44. log.Println("Received webhook, starting a refresh")
  45. go func() {
  46. fetchLatest()
  47. }()
  48. w.WriteHeader(http.StatusOK)
  49. return true
  50. }
  51. return false
  52. }
  53. func serveAssets(w http.ResponseWriter, request *http.Request) bool {
  54. if release == nil {
  55. w.WriteHeader(http.StatusInternalServerError)
  56. _, _ = fmt.Fprint(w, "Unknown release")
  57. return true
  58. }
  59. for _, asset := range release.Assets {
  60. if "/" + *asset.Name == request.RequestURI {
  61. temporaryRedirect(w, *asset.BrowserDownloadURL)
  62. return true
  63. }
  64. }
  65. return false
  66. }
  67. func serve(w http.ResponseWriter, request *http.Request) {
  68. if !serveStaticPaths(w, request) && !serveAssets(w, request) {
  69. w.WriteHeader(http.StatusNotFound)
  70. _, _ = fmt.Fprint(w, "Asset not found in release ", *release.Name)
  71. }
  72. }
  73. func parseRepo(fullRepo *string) error {
  74. if len(*fullRepo) == 0 {
  75. return fmt.Errorf("the repository option must be specified")
  76. }
  77. if strings.Count(*fullRepo, "/") != 1 {
  78. return fmt.Errorf("the repository must be specified in `user/repo` format")
  79. }
  80. repoParts := strings.Split(*fullRepo, "/")
  81. owner = repoParts[0]
  82. repo = repoParts[1]
  83. return nil
  84. }
  85. func initTicker(seconds int) {
  86. if seconds > 0 {
  87. log.Printf("Starting ticker for polling once every %d seconds.\n", seconds)
  88. ticker = time.NewTicker(time.Duration(seconds) * time.Second)
  89. go func() {
  90. for range ticker.C {
  91. fetchLatest()
  92. }
  93. }()
  94. } else {
  95. log.Println("Not starting ticker; performing one off fetch and relying on webhooks.")
  96. }
  97. fetchLatest()
  98. }
  99. func main() {
  100. redirect = flag.String("redirect", "", "if specified, requests for / will be redirected to this url")
  101. webhookPath = flag.String("webhook", "", "full path to receive release webhooks from GitHub on")
  102. var fullRepo = flag.String("repo", "", "the repository to redirect releases for, in user/repo format [required]")
  103. var port = flag.Int("port", 8080, "the port to listen on for HTTP requests")
  104. var poll = flag.Int("poll", 3600, "the amount of time to wait between polling for releases; 0 to disable polling")
  105. flag.Parse()
  106. if err := parseRepo(fullRepo); err != nil {
  107. _, _ = fmt.Fprintf(os.Stderr, "Error: %s\n\n", err.Error())
  108. flag.Usage()
  109. return
  110. }
  111. client = github.NewClient(nil)
  112. var cancel context.CancelFunc
  113. ctx, cancel = context.WithCancel(context.Background())
  114. c := make(chan os.Signal, 1)
  115. signal.Notify(c, os.Interrupt)
  116. signal.Notify(c, syscall.SIGTERM)
  117. go func() {
  118. for sig := range c {
  119. cancel()
  120. if ticker != nil {
  121. ticker.Stop()
  122. }
  123. log.Printf("Received %s, exiting.\n", sig.String())
  124. os.Exit(0)
  125. }
  126. }()
  127. initTicker(*poll)
  128. log.Printf("Listing on :%d\n", *port)
  129. server := &http.Server{
  130. Addr: fmt.Sprintf(":%d", *port),
  131. Handler: http.HandlerFunc(serve),
  132. }
  133. err := server.ListenAndServe()
  134. if err != nil {
  135. log.Println("Error listening for requests on port ", port, err)
  136. }
  137. }