Skip to content

Commit

Permalink
refactor: improving http transport layer (#51)
Browse files Browse the repository at this point in the history
  • Loading branch information
CallumKerson committed Apr 18, 2023
1 parent 990d803 commit a65e3df
Show file tree
Hide file tree
Showing 16 changed files with 202 additions and 179 deletions.
15 changes: 13 additions & 2 deletions cmd/athenaeum/cmd.go
Original file line number Diff line number Diff line change
Expand Up @@ -107,9 +107,20 @@ func runServer(cfg *Config) error {

var httpHandler *transportHttp.Handler
if cfg.Cache.Enabled {
httpHandler = transportHttp.NewHandler(podcastSvc, memcache.NewStore(cfg.GetMemcacheOpts()...), logger, cfg.GetHTTPHandlerOpts()...)
httpHandler = transportHttp.NewHandler(
podcastSvc,
cfg.Media.Root,
transportHttp.WithCacheStore(memcache.NewStore(cfg.GetMemcacheOpts()...)),
transportHttp.WithLogger(logger),
transportHttp.WithVersion(Version),
)
} else {
httpHandler = transportHttp.NewHandler(podcastSvc, nil, logger, cfg.GetHTTPHandlerOpts()...)
httpHandler = transportHttp.NewHandler(
podcastSvc,
cfg.Media.Root,
transportHttp.WithLogger(logger),
transportHttp.WithVersion(Version),
)
}

return transportHttp.Serve(httpHandler, cfg.Port, logger)
Expand Down
28 changes: 22 additions & 6 deletions cmd/athenaeum/cmd_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,45 +29,61 @@ func TestRootCommand(t *testing.T) {
expectedContentType string
expectedBody string
}{
{
name: "index",
path: "/",
method: "GET",
expectedStatus: 200,
expectedContentType: "text/html; charset=utf-8",
expectedBody: getExpected(t, "index.html", nil),
},
{
name: "version",
path: "/version",
method: "GET",
expectedStatus: 200,
expectedContentType: "application/json; charset=utf-8",
expectedBody: fmt.Sprintf("{\n \"version\": %q\n}", Version),
},
{
name: "feed",
path: "/podcast/feed.rss",
method: "GET",
expectedStatus: 200,
expectedContentType: "text/xml; charset=utf-8",
expectedBody: getExpectedFeed(t, "expected.rss", host),
expectedBody: getExpected(t, "expected.rss", host),
},
{
name: "sci-fi feed",
path: "/podcast/genre/lgbt+/feed.rss",
method: "GET",
expectedStatus: 200,
expectedContentType: "text/xml; charset=utf-8",
expectedBody: getExpectedFeed(t, "lgbt.rss", host),
expectedBody: getExpected(t, "lgbt.rss", host),
},
{
name: "author feed",
path: "/podcast/authors/Ursula%20K.%20Le%20Guin/feed.rss",
method: "GET",
expectedStatus: 200,
expectedContentType: "text/xml; charset=utf-8",
expectedBody: getExpectedFeed(t, "le_guin.rss", host),
expectedBody: getExpected(t, "le_guin.rss", host),
},
{
name: "narrator feed",
path: "/podcast/narrators/Emily%20Woo%20Zeller/feed.rss",
method: "GET",
expectedStatus: 200,
expectedContentType: "text/xml; charset=utf-8",
expectedBody: getExpectedFeed(t, "woo_zeller.rss", host),
expectedBody: getExpected(t, "woo_zeller.rss", host),
},
{
name: "tag feed",
path: "/podcast/tags/Hugo%20Awards/feed.rss",
method: "GET",
expectedStatus: 200,
expectedContentType: "text/xml; charset=utf-8",
expectedBody: getExpectedFeed(t, "hugo_awards.rss", host),
expectedBody: getExpected(t, "hugo_awards.rss", host),
},
}

Expand Down Expand Up @@ -127,7 +143,7 @@ func getFreePort(t *testing.T) int {
return l.Addr().(*net.TCPAddr).Port
}

func getExpectedFeed(t *testing.T, filename string, host interface{}) string {
func getExpected(t *testing.T, filename string, host interface{}) string {
var b bytes.Buffer
tpl, err := template.ParseFiles(filepath.Join("testdata", filename))
assert.NoError(t, err)
Expand Down
3 changes: 1 addition & 2 deletions cmd/athenaeum/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,7 @@ type DB struct {
}

type Media struct {
Root string
HostPath string
Root string
}

type ThirdParty struct {
Expand Down
20 changes: 2 additions & 18 deletions cmd/athenaeum/config_opts.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,14 +13,6 @@ import (
"github.com/CallumKerson/Athenaeum/pkg/client"
)

const (
staticPath = "static"
)

func (c *Config) GetMediaHost() string {
return fmt.Sprintf("%s/%s", c.Host, c.Media.HostPath)
}

func (c *Config) GetMediaServiceOpts() []mediaService.Option {
return []mediaService.Option{mediaService.WithPathToMediaRoot(c.Media.Root)}
}
Expand All @@ -31,20 +23,12 @@ func (c *Config) GetBoltDBOps() []bolt.Option {

func (c *Config) GetPodcastServiceOpts() []podcastService.Option {
return []podcastService.Option{podcastService.WithHost(c.Host),
podcastService.WithMediaPath(c.Media.HostPath),
podcastService.WithMediaPath(transportHttp.MediaPath),
podcastService.WithPodcastFeedInfo(c.Podcast.Explicit, c.Podcast.Language, c.Podcast.Author, c.Podcast.Email, c.Podcast.Copyright,
strings.Join([]string{c.Host, staticPath, "itunes_image.jpg"}, "/")),
fmt.Sprintf("%s%s/itunes_image.jpg", c.Host, transportHttp.StaticPath)),
podcastService.WithHandlePreUnixEpoch(c.Podcast.PreUnixEpoch.Handle)}
}

func (c *Config) GetHTTPHandlerOpts() []transportHttp.HandlerOption {
return []transportHttp.HandlerOption{
transportHttp.WithMediaConfig(c.Media.Root, c.Media.HostPath),
transportHttp.WithVersion(Version),
transportHttp.WithStaticPath(staticPath),
}
}

func (c *Config) GetClientOpts() []client.Option {
return []client.Option{client.WithHost(c.Host), client.WithVersion(Version)}
}
Expand Down
1 change: 0 additions & 1 deletion cmd/athenaeum/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,6 @@ func TestConfig_DefaultsOnly(t *testing.T) {
assert.Equal(t, "None", config.Podcast.Copyright)
assert.Equal(t, true, config.Podcast.Explicit)
assert.Equal(t, "EN", config.Podcast.Language)
assert.Equal(t, "/media", config.Media.HostPath)
assert.Equal(t, "/srv/media", config.Media.Root)
assert.Equal(t, "http://localhost:8080", config.Host)
assert.Equal(t, "INFO", config.Log.Level)
Expand Down
43 changes: 43 additions & 0 deletions cmd/athenaeum/testdata/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
<html>

<head>
<title>Audiobooks 🎧📚</title>
<meta charset='utf-8'>
<style>
body {
font-family: Helvetica, Arial, sans-serif;
}

.message {
width: 330px;
padding: 20px 40px;
margin: 0 auto;
background-color: #f9f9f9;
border: 1px solid #ddd;
}

center {
margin: 40px 0;
}

h1 {
font-size: 18px;
line-height: 26px;
}

p {
font-size: 12px;
}
</style>
</head>

<body>
<div class="message">
<h1>Audiobooks 🎧📚</h1>
<img src="/static/itunes_image_small.jpg" alt="Audiobooks" width="300" border="4">
<p>Like movies in your mind!</p>
<p>Feed link <a href="/podcast/feed.rss">here</a>.</p>
</div>
</body>

</html>
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ module github.com/CallumKerson/Athenaeum
go 1.20

require (
github.com/CallumKerson/loggerrific v1.0.1
github.com/CallumKerson/loggerrific v1.1.0
github.com/CallumKerson/podcasts v0.0.5
github.com/alfg/mp4 v0.0.0-20210728035756-55ea58c08aeb
github.com/carlmjohnson/requests v0.23.2
Expand Down
4 changes: 2 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -38,8 +38,8 @@ cloud.google.com/go/storage v1.14.0/go.mod h1:GrKmX003DSIwi9o29oFT7YDnHYwZoctc3f
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
github.com/CallumKerson/loggerrific v1.0.1 h1:ooe171INnDGUku/7eXe2aX6xJmCNHSLsyZexNJvrTH0=
github.com/CallumKerson/loggerrific v1.0.1/go.mod h1:jJg0jUNE6bIEqSOuPuNG+YBZVCsTOb80/7x18bz7Sqk=
github.com/CallumKerson/loggerrific v1.1.0 h1:3rnwDCMpwOHj9m4sK6KtN0U5vrw5UxyPjZZpj8V0Li0=
github.com/CallumKerson/loggerrific v1.1.0/go.mod h1:jJg0jUNE6bIEqSOuPuNG+YBZVCsTOb80/7x18bz7Sqk=
github.com/CallumKerson/podcasts v0.0.5 h1:Pjt2IIUQIwPbRvaS2OmxWKi2wVF1LrHiOUy55dMzO08=
github.com/CallumKerson/podcasts v0.0.5/go.mod h1:SGDAg6H7VevfPLGtEQ2y5HvA+5h2rJKiCkh+ygGeRhc=
github.com/alfg/mp4 v0.0.0-20210728035756-55ea58c08aeb h1:v8z8Yym2Z/NCYgXO/aFqmYDYa/d3uRyVMq1DjgMfzn4=
Expand Down
78 changes: 46 additions & 32 deletions internal/transport/http/handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,17 +5,26 @@ import (
"fmt"
"io"
"net/http"
"strings"
"time"

"github.com/go-chi/chi/v5"
chiMiddleware "github.com/go-chi/chi/v5/middleware"

"github.com/CallumKerson/loggerrific"
noOptLogger "github.com/CallumKerson/loggerrific/noopt"

"github.com/CallumKerson/Athenaeum/pkg/audiobooks"
"github.com/CallumKerson/Athenaeum/static"
)

const (
StaticPath = "/static"
PodcastPath = "/podcast"
MediaPath = "/media"
PodcastFeedName = "feed.rss"
)

type AudiobooksPodcastService interface {
WriteAllAudiobooksFeed(context.Context, io.Writer) error
WriteGenreAudiobookFeed(context.Context, audiobooks.Genre, io.Writer) error
Expand All @@ -36,63 +45,68 @@ type CacheStore interface {

type Handler struct {
*chi.Mux
PodcastService AudiobooksPodcastService
CacheStore CacheStore
Log loggerrific.Logger
version string
mediaRoot string
mediaServePath string
staticServePath string
podcastServePath string
mainFeedPath string
PodcastService AudiobooksPodcastService
mediaRoot string
CacheStore CacheStore
Log loggerrific.Logger
version string
}

func NewHandler(podcastService AudiobooksPodcastService, cacheStore CacheStore,
logger loggerrific.Logger, opts ...HandlerOption) *Handler {
func NewHandler(podcastService AudiobooksPodcastService, mediaRoot string, opts ...HandlerOption) *Handler {
handler := &Handler{
PodcastService: podcastService,
CacheStore: cacheStore,
Log: logger,
Log: noOptLogger.New(),
mediaRoot: mediaRoot,
}
for _, opt := range opts {
opt(handler)
}
handler.podcastServePath = "/podcast"
handler.mainFeedPath = "/feed.rss"
handler.Mux = chi.NewRouter()
handler.mapRoutes()
return handler
}

func (h *Handler) mapRoutes() {
middleware := NewMiddlewares(h.Log, h.CacheStore)
h.Use(chiMiddleware.RequestID, chiMiddleware.Recoverer, middleware.LoggingMiddleware, TimeoutMiddleware)
h.Use(chiMiddleware.RequestID, chiMiddleware.Recoverer, GetLoggingMiddleware(h.Log), TimeoutMiddleware)

h.HandleFunc("/health", healthCheck)
h.HandleFunc("/ready", h.readiness)
h.HandleFunc("/version", h.printVersion)

h.Route(h.podcastServePath, func(router chi.Router) {
if middleware.CacheStore != nil {
h.Log.Infoln("Caching enabled on", h.podcastServePath, "endpoints is enabled with at TTL of", middleware.CacheStore.GetTTL().String())
h.Route(PodcastPath, func(router chi.Router) {
if h.CacheStore != nil {
h.Log.Infoln("Caching enabled on", PodcastPath, "endpoints is enabled with at TTL of", h.CacheStore.GetTTL().String())
router.Use(GetCachingMiddleware(h.CacheStore))
}
router.Use(middleware.CachingMiddleware)
router.HandleFunc(fmt.Sprintf("/genre/{genre}%s", h.mainFeedPath), h.getGenreFeed)
router.HandleFunc(fmt.Sprintf("/authors/{author}%s", h.mainFeedPath), h.getAuthorFeed)
router.HandleFunc(fmt.Sprintf("/narrators/{narrator}%s", h.mainFeedPath), h.getNarratorFeed)
router.HandleFunc(fmt.Sprintf("/tags/{tag}%s", h.mainFeedPath), h.getTagFeed)
router.HandleFunc(h.mainFeedPath, h.getFeed)
router.HandleFunc(fmt.Sprintf("/genre/{genre}/%s", PodcastFeedName), h.getGenreFeed)
router.HandleFunc(fmt.Sprintf("/authors/{author}/%s", PodcastFeedName), h.getAuthorFeed)
router.HandleFunc(fmt.Sprintf("/narrators/{narrator}/%s", PodcastFeedName), h.getNarratorFeed)
router.HandleFunc(fmt.Sprintf("/tags/{tag}/%s", PodcastFeedName), h.getTagFeed)
router.HandleFunc(fmt.Sprintf("/%s", PodcastFeedName), h.getFeed)
})

h.Handle("/update", SevereRateLimitMiddleware(http.HandlerFunc(h.updateAudiobooks)))

mediaFS := http.StripPrefix(h.mediaServePath, http.FileServer(http.Dir(h.mediaRoot)))
h.Log.Infoln("Serving media files from local path", h.mediaRoot, "at", h.mediaServePath)
h.Handle(fmt.Sprintf("%s*", h.mediaServePath), mediaFS)
h.Log.Infoln("Serving media files from local path", h.mediaRoot, "at", MediaPath)
h.routeFileServer(MediaPath, http.Dir(h.mediaRoot))

staticFS := http.StripPrefix(h.staticServePath, http.FileServer(http.FS(static.Assets)))
h.Log.Infoln("Serving static files at", h.staticServePath)
h.Handle(fmt.Sprintf("%s*", h.staticServePath), staticFS)
h.Log.Infoln("Serving static files at", StaticPath)
h.routeFileServer(StaticPath, http.FS(static.Assets))

h.HandleFunc("/", h.serveHTML)
}

func (h *Handler) routeFileServer(path string, root http.FileSystem) {
if path != "/" && path[len(path)-1] != '/' {
h.Get(path, http.RedirectHandler(path+"/", http.StatusMovedPermanently).ServeHTTP)
path += "/"
}
path += "*"

h.Get(path, func(w http.ResponseWriter, r *http.Request) {
routeContext := chi.RouteContext(r.Context())
pathPrefix := strings.TrimSuffix(routeContext.RoutePattern(), "/*")
fs := http.StripPrefix(pathPrefix, http.FileServer(root))
fs.ServeHTTP(w, r)
})
}
12 changes: 7 additions & 5 deletions internal/transport/http/handler_funcs.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,9 +33,11 @@ func (h *Handler) readiness(writer http.ResponseWriter, request *http.Request) {
}

func (h *Handler) printVersion(writer http.ResponseWriter, request *http.Request) {
SendJSON(writer, http.StatusOK, Payload{
"version": h.version,
})
if h.version != "" {
SendJSON(writer, http.StatusOK, Payload{
"version": h.version,
})
}
}

func (h *Handler) getFeed(writer http.ResponseWriter, request *http.Request) {
Expand Down Expand Up @@ -127,8 +129,8 @@ func (h *Handler) serveHTML(writer http.ResponseWriter, request *http.Request) {
data := map[string]interface{}{
"Title": "Audiobooks",
"Description": "Like movies in your mind!",
"StaticServePath": h.staticServePath,
"FeedLink": fmt.Sprintf("%s%s", h.podcastServePath, h.mainFeedPath),
"StaticServePath": StaticPath,
"FeedLink": fmt.Sprintf("%s/%s", PodcastPath, PodcastFeedName),
}
if err := tpl.Execute(writer, data); err != nil {
return
Expand Down
Loading

0 comments on commit a65e3df

Please sign in to comment.