package main import ( "encoding/json" "fmt" "net/http" "net/url" "sort" "strconv" "strings" "time" "github.com/gorilla/feeds" "github.com/gorilla/mux" "github.com/pkg/errors" "github.com/sirupsen/logrus" "github.com/Luzifer/go-latestver/internal/badge" "github.com/Luzifer/go-latestver/internal/config" "github.com/Luzifer/go-latestver/internal/database" "github.com/Luzifer/go-latestver/internal/fetcher" ) type ( apiCatalogEntry struct { database.CatalogEntry database.CatalogMeta } ) func buildFullURL(u *url.URL, _ error) string { return strings.Join([]string{ strings.TrimRight(cfg.BaseURL, "/"), strings.TrimLeft(u.String(), "/"), }, "/") } func catalogEntryToAPICatalogEntry(ce database.CatalogEntry) (apiCatalogEntry, error) { cm, err := storage.Catalog.GetMeta(&ce) if err != nil { return apiCatalogEntry{}, errors.Wrap(err, "fetching catalog meta") } for _, l := range fetcher.Get(ce.Fetcher).Links(ce.FetcherConfig) { var found bool for _, el := range ce.Links { if l.Name == el.Name { found = true break } } if !found { ce.Links = append(ce.Links, l) } } return apiCatalogEntry{CatalogEntry: ce, CatalogMeta: *cm}, nil } func handleBadge(w http.ResponseWriter, r *http.Request) { var ( compare = r.FormValue("compare") vars = mux.Vars(r) name, tag = vars["name"], vars["tag"] ) ce, err := configFile.CatalogEntryByTag(name, tag) if errors.Is(err, config.ErrCatalogEntryNotFound) { http.Error(w, "Not found", http.StatusNotFound) return } cm, err := storage.Catalog.GetMeta(&ce) if err != nil { http.Error(w, "Unable to fetch catalog data", http.StatusInternalServerError) return } color := "green" if compare != "" && compare != cm.CurrentVersion { color = "red" } svg := badge.Create(ce.Key(), cm.CurrentVersion, color) w.Header().Add("Content-Type", "image/svg+xml") if _, err = w.Write(svg); err != nil { logrus.WithError(err).Error("writing SVG response") } } func handleCatalogGet(w http.ResponseWriter, r *http.Request) { var ( vars = mux.Vars(r) name, tag = vars["name"], vars["tag"] ) ce, err := configFile.CatalogEntryByTag(name, tag) if errors.Is(err, config.ErrCatalogEntryNotFound) { http.Error(w, "Not found", http.StatusNotFound) return } ae, err := catalogEntryToAPICatalogEntry(ce) if err != nil { logrus.WithError(err).Error("Unable to fetch catalog data") http.Error(w, "Unable to fetch catalog data", http.StatusInternalServerError) return } w.Header().Set("Content-Type", "application/json") if err = json.NewEncoder(w).Encode(ae); err != nil { logrus.WithError(err).Error("Unable to encode catalog entry") http.Error(w, "Unable to encode catalog meta", http.StatusInternalServerError) return } } func handleCatalogGetVersion(w http.ResponseWriter, r *http.Request) { var ( vars = mux.Vars(r) name, tag = vars["name"], vars["tag"] ) ce, err := configFile.CatalogEntryByTag(name, tag) if errors.Is(err, config.ErrCatalogEntryNotFound) { http.Error(w, "Not found", http.StatusNotFound) return } cm, err := storage.Catalog.GetMeta(&ce) if err != nil { logrus.WithError(err).Error("Unable to fetch catalog meta") http.Error(w, "Unable to fetch catalog meta", http.StatusInternalServerError) return } w.Header().Set("Content-Type", "text/plain") fmt.Fprint(w, cm.CurrentVersion) } func handleCatalogList(w http.ResponseWriter, _ *http.Request) { out := make([]apiCatalogEntry, len(configFile.Catalog)) for i := range configFile.Catalog { ce := configFile.Catalog[i] ae, err := catalogEntryToAPICatalogEntry(ce) if err != nil { logrus.WithError(err).Error("Unable to fetch catalog data") http.Error(w, "Unable to fetch catalog data", http.StatusInternalServerError) return } out[i] = ae } sort.Slice(out, func(i, j int) bool { return out[i].Key() < out[j].Key() }) w.Header().Set("Content-Type", "application/json") if err := json.NewEncoder(w).Encode(out); err != nil { logrus.WithError(err).Error("Unable to encode catalog entry list") http.Error(w, "Unable to encode catalog meta", http.StatusInternalServerError) return } } func handleLog(w http.ResponseWriter, r *http.Request) { logs, err := prepareLogForRequest(r) switch err { case nil: // This is fine case config.ErrCatalogEntryNotFound: http.Error(w, "Not found", http.StatusNotFound) default: http.Error(w, "Unable to fetch logs", http.StatusInternalServerError) return } w.Header().Set("Content-Type", "application/json") if err = json.NewEncoder(w).Encode(logs); err != nil { logrus.WithError(err).Error("Unable to encode logs") http.Error(w, "Unable to encode logs", http.StatusInternalServerError) return } } func handleLogFeed(w http.ResponseWriter, r *http.Request) { vars := mux.Vars(r) logs, err := prepareLogForRequest(r) switch err { case nil: // This is fine case config.ErrCatalogEntryNotFound: http.Error(w, "Not found", http.StatusNotFound) default: http.Error(w, "Unable to fetch logs", http.StatusInternalServerError) return } feed := &feeds.Feed{ Description: "Generated by go-latestver: https://github.com/Luzifer/go-latestver", Link: &feeds.Link{Href: buildFullURL(router.Get("catalog").URL())}, Title: "Latestver Update Log", } if vars["name"] != "" { feed.Title = fmt.Sprintf("Latestver Update Log of %s:%s", vars["name"], vars["tag"]) feed.Link.Href = buildFullURL(router.Get("catalog-entry").URL("name", vars["name"], "tag", vars["tag"])) } for _, le := range logs { feed.Add(&feeds.Item{ Created: le.Timestamp.UTC(), Description: fmt.Sprintf("%s:%s updated to version %s from %s", le.CatalogName, le.CatalogTag, le.VersionTo, le.VersionFrom), Id: fmt.Sprintf("%s:%s-%s", le.CatalogName, le.CatalogTag, le.Timestamp.UTC().Format(time.RFC3339)), Link: &feeds.Link{Href: buildFullURL(router.Get("catalog-entry").URL("name", le.CatalogName, "tag", le.CatalogTag))}, Title: fmt.Sprintf("%s:%s %s", le.CatalogName, le.CatalogTag, le.VersionTo), }) } w.Header().Set("Content-Type", "application/rss+xml; charset=utf-8") if err = feed.WriteRss(w); err != nil { logrus.WithError(err).Error("Unable to render RSS") http.Error(w, "Unable to render RSS", http.StatusInternalServerError) return } } func prepareLogForRequest(r *http.Request) ([]database.LogEntry, error) { var ( vars = mux.Vars(r) name, tag = vars["name"], vars["tag"] num, page = 25, 0 ce database.CatalogEntry err error logs []database.LogEntry ) if v, err := strconv.Atoi(r.FormValue("num")); err == nil && v > 0 && v < 100 { num = v } if v, err := strconv.Atoi(r.FormValue("page")); err == nil && v >= 0 { page = v } if name == "" && tag == "" { logs, err = storage.Logs.List(num, page) } else { ce, err = configFile.CatalogEntryByTag(name, tag) if errors.Is(err, config.ErrCatalogEntryNotFound) { return nil, config.ErrCatalogEntryNotFound } logs, err = storage.Logs.ListForCatalogEntry(&ce, num, page) } return logs, errors.Wrap(err, "listing log entries") }